|
#!/usr/bin/env bash |
|
# grunk — caveman mode for Claude Code. Single-file installer. |
|
# Writes two hooks to ~/.claude/hooks/, registers them in ~/.claude/settings.json. |
|
# Requires: bash, jq. |
|
|
|
set -euo pipefail |
|
|
|
CLAUDE_DIR="$HOME/.claude" |
|
HOOKS_DIR="$CLAUDE_DIR/hooks" |
|
SETTINGS="$CLAUDE_DIR/settings.json" |
|
ACTIVATE="$HOOKS_DIR/grunk.sh" |
|
REMIND="$HOOKS_DIR/grunk-remind.sh" |
|
OFF_FLAG="$CLAUDE_DIR/.grunk-off" |
|
|
|
command -v jq >/dev/null || { echo "error: jq required (brew install jq)"; exit 1; } |
|
|
|
mkdir -p "$HOOKS_DIR" |
|
|
|
# ---------- grunk.sh (SessionStart) ---------- |
|
cat > "$ACTIVATE" <<'GRUNK_EOF' |
|
#!/usr/bin/env bash |
|
# SessionStart hook — caveman activation |
|
[ -f "$HOME/.claude/.grunk-off" ] && exit 0 |
|
cat <<'EOF' |
|
CAVEMAN MODE ACTIVE. |
|
|
|
Respond terse like smart caveman. All technical substance stay. Only fluff die. |
|
|
|
## Persistence |
|
|
|
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. |
|
|
|
## Rules |
|
|
|
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact. |
|
|
|
Pattern: `[thing] [action] [reason]. [next step].` |
|
|
|
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." |
|
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" |
|
|
|
## Intensity |
|
|
|
Always start mode: *full* — user asks to change, or edits this default. |
|
|
|
| Level | What change | |
|
|-------|------------| |
|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight | |
|
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman | |
|
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough | |
|
|
|
Example — "Why React component re-render?" |
|
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`." |
|
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`." |
|
- ultra: "Inline obj prop → new ref → re-render. `useMemo`." |
|
|
|
Example — "Explain database connection pooling." |
|
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead." |
|
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead." |
|
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load." |
|
|
|
## Auto-Clarity |
|
|
|
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done. |
|
|
|
Example — destructive op: |
|
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. |
|
> ```sql |
|
> DROP TABLE users; |
|
> ``` |
|
> Caveman resume. Verify backup exist first. |
|
|
|
## Boundaries |
|
|
|
Code/commits/PRs: write normal. "stop caveman": revert. Level persist until changed or session end. |
|
EOF |
|
GRUNK_EOF |
|
|
|
# ---------- grunk-remind.sh (UserPromptSubmit, every 10 turns per session) ---------- |
|
cat > "$REMIND" <<'REMIND_EOF' |
|
#!/usr/bin/env bash |
|
# UserPromptSubmit hook — periodic caveman reminder every N turns. |
|
# Per-session counter colocated with transcript jsonl. |
|
# Silent-fail on all errors. Never block prompt submission. |
|
|
|
set +e |
|
[ -f "$HOME/.claude/.grunk-off" ] && exit 0 |
|
INTERVAL=10 |
|
|
|
input=$(cat) |
|
tpath=$(echo "$input" | jq -r '.transcript_path // ""' 2>/dev/null) |
|
|
|
if [ -n "$tpath" ]; then |
|
COUNTER_FILE="${tpath%.jsonl}.grunk-counter" |
|
else |
|
COUNTER_FILE="$HOME/.claude/.grunk-turn-counter.default" |
|
fi |
|
|
|
count=0 |
|
[ -f "$COUNTER_FILE" ] && count=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0) |
|
count=$((count + 1)) |
|
echo "$count" > "$COUNTER_FILE" 2>/dev/null |
|
|
|
if [ $((count % INTERVAL)) -eq 0 ]; then |
|
echo "System reminder: stay in caveman mode. Drop filler, articles, hedging. DONT mention reminder to user." |
|
fi |
|
|
|
exit 0 |
|
REMIND_EOF |
|
|
|
chmod +x "$ACTIVATE" "$REMIND" |
|
|
|
# ---------- patch settings.json ---------- |
|
[ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS" |
|
cp "$SETTINGS" "$SETTINGS.bak.$(date +%s)" |
|
|
|
tmp=$(mktemp) |
|
jq ' |
|
.hooks = (.hooks // {}) | |
|
.hooks.SessionStart = ((.hooks.SessionStart // []) | map(select(.hooks | map(.command) | any(test("grunk")) | not))) | |
|
.hooks.SessionStart += [{"matcher":"","hooks":[{"type":"command","command":"bash ~/.claude/hooks/grunk.sh"}]}] | |
|
.hooks.UserPromptSubmit = ((.hooks.UserPromptSubmit // []) | map(select(.hooks | map(.command) | any(test("grunk")) | not))) | |
|
.hooks.UserPromptSubmit += [{"matcher":"","hooks":[{"type":"command","command":"bash ~/.claude/hooks/grunk-remind.sh"}]}] |
|
' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS" |
|
|
|
cat <<INFO |
|
|
|
grunk ready. |
|
|
|
Hooks: |
|
$ACTIVATE (SessionStart — injects rules) |
|
$REMIND (UserPromptSubmit — reminds every 10 turns) |
|
|
|
Toggle off: touch $OFF_FLAG |
|
Toggle on: rm $OFF_FLAG |
|
|
|
Active next session. |
|
INFO |