cc-safe-setup 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # cc-safe-setup
2
+
3
+ **One command to make Claude Code safe for autonomous operation.**
4
+
5
+ ```bash
6
+ npx cc-safe-setup
7
+ ```
8
+
9
+ Installs 4 production-tested safety hooks in ~10 seconds. Zero dependencies. No manual configuration.
10
+
11
+ ## Why This Exists
12
+
13
+ A Claude Code user [lost their entire C:\Users directory](https://github.com/anthropics/claude-code/issues/36339) when `rm -rf` followed NTFS junctions. Another had untested code pushed to main at 3am. Syntax errors cascaded through 30+ files before anyone noticed.
14
+
15
+ Claude Code ships with no safety hooks by default. This tool fixes that.
16
+
17
+ ## What Gets Installed
18
+
19
+ | Hook | Prevents | Trigger |
20
+ |------|----------|---------|
21
+ | **Destructive Guard** | `rm -rf /`, `git reset --hard`, `git clean -fd` | PreToolUse (Bash) |
22
+ | **Branch Guard** | Direct pushes to main/master | PreToolUse (Bash) |
23
+ | **Syntax Check** | Python, Shell, JSON, YAML, JS errors after edits | PostToolUse (Edit\|Write) |
24
+ | **Context Monitor** | Session state loss from context window overflow | PostToolUse |
25
+
26
+ Each hook exists because a real incident happened without it.
27
+
28
+ ## How It Works
29
+
30
+ 1. Writes hook scripts to `~/.claude/hooks/`
31
+ 2. Updates `~/.claude/settings.json` to register the hooks
32
+ 3. Restart Claude Code — hooks are active
33
+
34
+ Safe to run multiple times. Existing settings are preserved. A backup is created if settings.json can't be parsed.
35
+
36
+ ## After Installing
37
+
38
+ Verify your setup:
39
+
40
+ ```bash
41
+ npx cc-health-check
42
+ ```
43
+
44
+ ## Full Kit
45
+
46
+ cc-safe-setup gives you 4 essential hooks. For the complete autonomous operation toolkit:
47
+
48
+ **[Claude Code Ops Kit](https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=github&utm_medium=readme&utm_campaign=safe-setup)** ($19) — 11 hooks + 6 templates + 3 exclusive tools + install.sh. Production-ready in 15 minutes.
49
+
50
+ Or start with the free hooks: [claude-code-hooks](https://github.com/yurukusa/claude-code-hooks)
51
+
52
+ ## License
53
+
54
+ MIT
package/index.mjs CHANGED
@@ -4,7 +4,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'f
4
4
  import { join, dirname } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { createInterface } from 'readline';
7
+ import { fileURLToPath } from 'url';
7
8
 
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
10
  const HOME = homedir();
9
11
  const HOOKS_DIR = join(HOME, '.claude', 'hooks');
10
12
  const SETTINGS_PATH = join(HOME, '.claude', 'settings.json');
@@ -12,112 +14,34 @@ const SETTINGS_PATH = join(HOME, '.claude', 'settings.json');
12
14
  const c = {
13
15
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
14
16
  red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
15
- blue: '\x1b[36m', magenta: '\x1b[35m',
17
+ blue: '\x1b[36m',
16
18
  };
17
19
 
18
- // ═══════════════════════════════════════════════════
19
- // Hook definitions — each one prevents a real incident
20
- // ═══════════════════════════════════════════════════
20
+ const SCRIPTS = JSON.parse(readFileSync(join(__dirname, 'scripts.json'), 'utf-8'));
21
21
 
22
22
  const HOOKS = {
23
23
  'destructive-guard': {
24
24
  name: 'Destructive Command Blocker',
25
- why: 'A user lost their entire C:\\Users directory when rm -rf followed NTFS junctions (GitHub #36339)',
26
- trigger: 'PreToolUse',
27
- matcher: 'Bash',
28
- script: `#!/bin/bash
29
- # destructive-guard.sh — Blocks rm -rf, git reset --hard, git clean
30
- INPUT=$(cat)
31
- COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
32
- [[ -z "$COMMAND" ]] && exit 0
33
-
34
- # rm on sensitive paths
35
- if echo "$COMMAND" | grep -qE 'rm\\s+(-[rf]+\\s+)*(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|~\\/|~\\s*$|\\.\\.\\/|\\.\\.\\s*$)'; then
36
- SAFE=0
37
- for dir in node_modules dist build .cache __pycache__ coverage; do
38
- echo "$COMMAND" | grep -qE "rm\\s+.*${dir}" && SAFE=1 && break
39
- done
40
- if (( SAFE == 0 )); then
41
- echo "BLOCKED: rm on sensitive path. On WSL2, rm -rf follows NTFS junctions." >&2
42
- exit 2
43
- fi
44
- fi
45
-
46
- # git reset --hard
47
- echo "$COMMAND" | grep -qE 'git\\s+reset\\s+--hard' && echo "BLOCKED: git reset --hard discards uncommitted changes." >&2 && exit 2
48
-
49
- # git clean -fd
50
- echo "$COMMAND" | grep -qE 'git\\s+clean\\s+-[a-z]*[fd]' && echo "BLOCKED: git clean removes untracked files permanently. Use -n first." >&2 && exit 2
51
-
52
- exit 0`,
25
+ why: 'A user lost their entire C:\\Users directory when rm -rf followed NTFS junctions',
26
+ trigger: 'PreToolUse', matcher: 'Bash',
53
27
  },
54
-
55
28
  'branch-guard': {
56
29
  name: 'Branch Push Protector',
57
30
  why: 'Autonomous Claude Code pushed untested code directly to main at 3am',
58
- trigger: 'PreToolUse',
59
- matcher: 'Bash',
60
- script: `#!/bin/bash
61
- # branch-guard.sh — Blocks pushes to main/master
62
- INPUT=$(cat)
63
- COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
64
- [[ -z "$COMMAND" ]] && exit 0
65
- echo "$COMMAND" | grep -qE '^\\s*git\\s+push' || exit 0
66
-
67
- PROTECTED="\${CC_PROTECT_BRANCHES:-main:master}"
68
- IFS=':' read -ra BRANCHES <<< "$PROTECTED"
69
- for branch in "\${BRANCHES[@]}"; do
70
- if echo "$COMMAND" | grep -qwE "origin\\s+\${branch}|\${branch}\\s|\${branch}$"; then
71
- echo "BLOCKED: Push to protected branch '\${branch}'. Use a feature branch + PR instead." >&2
72
- exit 2
73
- fi
74
- done
75
- exit 0`,
31
+ trigger: 'PreToolUse', matcher: 'Bash',
76
32
  },
77
-
78
33
  'syntax-check': {
79
34
  name: 'Post-Edit Syntax Validator',
80
35
  why: 'A Python syntax error cascaded through 30+ files before anyone noticed',
81
- trigger: 'PostToolUse',
82
- matcher: 'Edit|Write',
83
- script: `#!/bin/bash
84
- # syntax-check.sh — Validates syntax after file edits
85
- INPUT=$(cat)
86
- FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
87
- [[ -z "$FILE" || ! -f "$FILE" ]] && exit 0
88
-
89
- case "$FILE" in
90
- *.py) python3 -m py_compile "$FILE" 2>/dev/null || { echo "SYNTAX ERROR in $FILE" >&2; exit 1; } ;;
91
- *.sh) bash -n "$FILE" 2>/dev/null || { echo "SYNTAX ERROR in $FILE" >&2; exit 1; } ;;
92
- *.json) python3 -c "import json; json.load(open('$FILE'))" 2>/dev/null || { echo "INVALID JSON: $FILE" >&2; exit 1; } ;;
93
- *.yaml|*.yml) python3 -c "import yaml; yaml.safe_load(open('$FILE'))" 2>/dev/null || { echo "INVALID YAML: $FILE" >&2; exit 1; } ;;
94
- *.js|*.mjs) node --check "$FILE" 2>/dev/null || { echo "SYNTAX ERROR in $FILE" >&2; exit 1; } ;;
95
- esac
96
- exit 0`,
36
+ trigger: 'PostToolUse', matcher: 'Edit|Write',
97
37
  },
98
-
99
38
  'context-monitor': {
100
39
  name: 'Context Window Monitor',
101
40
  why: 'Sessions silently lost all state at tool call 150+ with no warning',
102
- trigger: 'PostToolUse',
103
- matcher: '',
104
- script: `#!/bin/bash
105
- # context-monitor.sh — Warns when context window is filling up
106
- INPUT=$(cat)
107
- PERCENT=$(echo "$INPUT" | jq -r '.session.context_window.percent_used // empty' 2>/dev/null)
108
- [[ -z "$PERCENT" ]] && exit 0
109
-
110
- if (( $(echo "$PERCENT > 80" | bc -l 2>/dev/null || echo 0) )); then
111
- echo "⚠️ Context window at \${PERCENT}%. Consider compacting or starting a new session." >&2
112
- fi
113
- exit 0`,
41
+ trigger: 'PostToolUse', matcher: '',
114
42
  },
115
43
  };
116
44
 
117
- // ═══════════════════════════════════════════════════
118
- // Installation logic
119
- // ═══════════════════════════════════════════════════
120
-
121
45
  function ask(question) {
122
46
  const rl = createInterface({ input: process.stdin, output: process.stdout });
123
47
  return new Promise(resolve => {
@@ -125,106 +49,77 @@ function ask(question) {
125
49
  });
126
50
  }
127
51
 
128
- function printHeader() {
52
+ async function main() {
129
53
  console.log();
130
- console.log(`${c.bold} cc-safe-setup${c.reset}`);
131
- console.log(`${c.dim} Make Claude Code safe for autonomous operation${c.reset}`);
54
+ console.log(c.bold + ' cc-safe-setup' + c.reset);
55
+ console.log(c.dim + ' Make Claude Code safe for autonomous operation' + c.reset);
132
56
  console.log();
133
- console.log(`${c.dim} This installs safety hooks that prevent real incidents:${c.reset}`);
134
- console.log(`${c.red} ✗${c.reset} rm -rf deleting entire user directories (NTFS junction traversal)`);
135
- console.log(`${c.red} ✗${c.reset} Untested code pushed to main at 3am`);
136
- console.log(`${c.red} ✗${c.reset} Syntax errors cascading through 30+ files`);
137
- console.log(`${c.red} ✗${c.reset} Sessions losing all context with no warning`);
57
+ console.log(c.dim + ' Prevents real incidents:' + c.reset);
58
+ console.log(c.red + ' x' + c.reset + ' rm -rf deleting entire user directories (NTFS junction traversal)');
59
+ console.log(c.red + ' x' + c.reset + ' Untested code pushed to main at 3am');
60
+ console.log(c.red + ' x' + c.reset + ' Syntax errors cascading through 30+ files');
61
+ console.log(c.red + ' x' + c.reset + ' Sessions losing all context with no warning');
138
62
  console.log();
139
- }
140
63
 
141
- function installHook(id, hook) {
142
- const hookPath = join(HOOKS_DIR, `${id}.sh`);
64
+ console.log(c.bold + ' Hooks to install:' + c.reset);
65
+ console.log();
66
+ for (const [id, hook] of Object.entries(HOOKS)) {
67
+ console.log(' ' + c.green + '*' + c.reset + ' ' + c.bold + hook.name + c.reset);
68
+ console.log(' ' + c.dim + hook.why + c.reset);
69
+ }
70
+ console.log();
71
+
72
+ const answer = await ask(' Install all ' + Object.keys(HOOKS).length + ' safety hooks? [Y/n] ');
73
+ if (answer.toLowerCase() === 'n') {
74
+ console.log('\n ' + c.dim + 'Cancelled.' + c.reset + '\n');
75
+ process.exit(0);
76
+ }
77
+
78
+ console.log();
143
79
  mkdirSync(HOOKS_DIR, { recursive: true });
144
- writeFileSync(hookPath, hook.script);
145
- try { chmodSync(hookPath, 0o755); } catch(e) {}
146
- return hookPath;
147
- }
148
80
 
149
- function updateSettings(installedHooks) {
81
+ for (const [id, hook] of Object.entries(HOOKS)) {
82
+ const hookPath = join(HOOKS_DIR, id + '.sh');
83
+ writeFileSync(hookPath, SCRIPTS[id]);
84
+ try { chmodSync(hookPath, 0o755); } catch(e) {}
85
+ console.log(' ' + c.green + 'v' + c.reset + ' ' + hook.name + ' -> ' + c.dim + hookPath + c.reset);
86
+ }
87
+
88
+ // Update settings.json
150
89
  let settings = {};
151
90
  if (existsSync(SETTINGS_PATH)) {
152
- try {
153
- settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
154
- } catch(e) {
155
- console.log(`${c.yellow} Warning: Could not parse existing settings.json. Creating backup.${c.reset}`);
91
+ try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch(e) {
156
92
  writeFileSync(SETTINGS_PATH + '.bak', readFileSync(SETTINGS_PATH));
157
- settings = {};
158
93
  }
159
94
  }
160
-
161
95
  if (!settings.hooks) settings.hooks = {};
162
96
 
163
- for (const [id, hook] of Object.entries(installedHooks)) {
97
+ for (const [id, hook] of Object.entries(HOOKS)) {
164
98
  const trigger = hook.trigger;
165
99
  if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
166
-
167
- const hookPath = join(HOOKS_DIR, `${id}.sh`);
168
- const entry = {
169
- matcher: hook.matcher,
170
- hooks: [{ type: 'command', command: hookPath }]
171
- };
172
-
173
- // Check if already exists
100
+ const hookPath = join(HOOKS_DIR, id + '.sh');
174
101
  const exists = settings.hooks[trigger].some(e =>
175
- e.hooks && e.hooks.some(h => h.command && h.command.includes(`${id}.sh`))
102
+ e.hooks && e.hooks.some(h => h.command && h.command.includes(id + '.sh'))
176
103
  );
177
-
178
104
  if (!exists) {
179
- settings.hooks[trigger].push(entry);
105
+ settings.hooks[trigger].push({
106
+ matcher: hook.matcher,
107
+ hooks: [{ type: 'command', command: hookPath }]
108
+ });
180
109
  }
181
110
  }
182
111
 
183
112
  mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
184
113
  writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
185
- }
186
-
187
- async function main() {
188
- printHeader();
189
-
190
- // Show what will be installed
191
- console.log(`${c.bold} Hooks to install:${c.reset}`);
192
- console.log();
193
- for (const [id, hook] of Object.entries(HOOKS)) {
194
- console.log(` ${c.green}●${c.reset} ${c.bold}${hook.name}${c.reset}`);
195
- console.log(` ${c.dim}${hook.why}${c.reset}`);
196
- }
197
- console.log();
114
+ console.log(' ' + c.green + 'v' + c.reset + ' settings.json updated -> ' + c.dim + SETTINGS_PATH + c.reset);
198
115
 
199
- const answer = await ask(` Install all ${Object.keys(HOOKS).length} safety hooks? [Y/n] `);
200
- if (answer.toLowerCase() === 'n') {
201
- console.log(`\n ${c.dim}Cancelled. No changes made.${c.reset}\n`);
202
- process.exit(0);
203
- }
204
-
205
- console.log();
206
-
207
- // Install hooks
208
- const installed = {};
209
- for (const [id, hook] of Object.entries(HOOKS)) {
210
- const path = installHook(id, hook);
211
- installed[id] = hook;
212
- console.log(` ${c.green}✓${c.reset} ${hook.name} → ${c.dim}${path}${c.reset}`);
213
- }
214
-
215
- // Update settings.json
216
- updateSettings(installed);
217
- console.log(` ${c.green}✓${c.reset} settings.json updated → ${c.dim}${SETTINGS_PATH}${c.reset}`);
218
-
219
- console.log();
220
- console.log(`${c.bold} Done.${c.reset} ${Object.keys(HOOKS).length} safety hooks installed.`);
221
- console.log();
222
- console.log(` ${c.dim}Restart Claude Code to activate the hooks.${c.reset}`);
223
116
  console.log();
224
- console.log(` ${c.dim}Verify your setup:${c.reset} ${c.blue}npx cc-health-check${c.reset}`);
117
+ console.log(c.bold + ' Done.' + c.reset + ' ' + Object.keys(HOOKS).length + ' safety hooks installed.');
118
+ console.log(' ' + c.dim + 'Restart Claude Code to activate.' + c.reset);
119
+ console.log(' ' + c.dim + 'Verify:' + c.reset + ' ' + c.blue + 'npx cc-health-check' + c.reset);
225
120
  console.log();
226
- console.log(` ${c.dim}Want more hooks + templates + tools?${c.reset}`);
227
- console.log(` ${c.bold}https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=npm&utm_medium=cli&utm_campaign=safe-setup${c.reset}`);
121
+ console.log(' ' + c.dim + 'Full kit (11 hooks + templates + tools):' + c.reset);
122
+ console.log(' https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=npm&utm_medium=cli&utm_campaign=safe-setup');
228
123
  console.log();
229
124
  }
230
125
 
package/package.json CHANGED
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "One command to make Claude Code safe for autonomous operation. Installs destructive command blockers, branch guards, and syntax checks.",
5
5
  "main": "index.mjs",
6
6
  "bin": {
7
7
  "cc-safe-setup": "index.mjs"
8
8
  },
9
- "keywords": ["claude-code", "ai", "safety", "hooks", "autonomous", "cli"],
9
+ "keywords": [
10
+ "claude-code",
11
+ "ai",
12
+ "safety",
13
+ "hooks",
14
+ "autonomous",
15
+ "cli"
16
+ ],
10
17
  "author": "yurukusa",
11
18
  "license": "MIT",
12
19
  "repository": {
package/scripts.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "destructive-guard": "#!/bin/bash\n# ================================================================\n# destructive-guard.sh \u2014 Destructive Command Blocker\n# ================================================================\n# PURPOSE:\n# Blocks dangerous shell commands that can cause irreversible damage.\n# Catches rm -rf on sensitive paths, git reset --hard, git clean -fd,\n# and other destructive operations before they execute.\n#\n# Built after a real incident where rm -rf on a pnpm project\n# followed NTFS junctions and deleted an entire C:\\Users directory.\n# (GitHub Issue #36339)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - rm -rf / rm -r on root, home, or parent paths (/, ~, .., /home, /etc)\n# - git reset --hard\n# - git clean -fd / git clean -fdx\n# - chmod -R 777 on sensitive paths\n# - find ... -delete on broad patterns\n#\n# WHAT IT ALLOWS (exit 0):\n# - rm -rf on specific project subdirectories (node_modules, dist, build)\n# - git reset --soft, git reset HEAD\n# - All non-destructive commands\n#\n# CONFIGURATION:\n# CC_ALLOW_DESTRUCTIVE=1 \u2014 disable this guard (not recommended)\n# CC_SAFE_DELETE_DIRS \u2014 colon-separated list of safe-to-delete dirs\n# default: \"node_modules:dist:build:.cache:__pycache__:coverage\"\n#\n# NOTE: On Windows/WSL2, rm -rf can follow NTFS junctions (symlinks)\n# and delete far more than intended. This guard is especially critical\n# on WSL2 environments.\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty')\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Allow override (not recommended)\nif [[ \"${CC_ALLOW_DESTRUCTIVE:-0}\" == \"1\" ]]; then\n exit 0\nfi\n\n# Safe directories that can be deleted\nSAFE_DIRS=\"${CC_SAFE_DELETE_DIRS:-node_modules:dist:build:.cache:__pycache__:coverage:.next:.nuxt:tmp}\"\n\n# --- Check 1: rm -rf on dangerous paths ---\nif echo \"$COMMAND\" | grep -qE 'rm\\s+(-[rf]+\\s+)*(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|\\/var|~\\/|~\\s*$|\\.\\.\\/|\\.\\.\\s*$)'; then\n # Exception: safe directories\n SAFE=0\n IFS=':' read -ra DIRS <<< \"$SAFE_DIRS\"\n for dir in \"${DIRS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"rm\\s+.*${dir}\\s*$|rm\\s+.*${dir}/\"; then\n SAFE=1\n break\n fi\n done\n\n if (( SAFE == 0 )); then\n echo \"BLOCKED: rm on sensitive path detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"This command targets a sensitive directory that could cause\" >&2\n echo \"irreversible data loss. On WSL2, rm -rf can follow NTFS\" >&2\n echo \"junctions and delete far beyond the target directory.\" >&2\n echo \"\" >&2\n echo \"If you need to delete a specific subdirectory, target it directly:\" >&2\n echo \" rm -rf ./specific-folder\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: git reset --hard ---\n# Only match when git is the actual command, not inside strings/arguments\nif echo \"$COMMAND\" | grep -qE '^\\s*git\\s+reset\\s+--hard|;\\s*git\\s+reset\\s+--hard|&&\\s*git\\s+reset\\s+--hard|\\|\\|\\s*git\\s+reset\\s+--hard'; then\n echo \"BLOCKED: git reset --hard discards all uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash, or git reset --soft to keep changes staged.\" >&2\n exit 2\nfi\n\n# --- Check 3: git clean -fd ---\nif echo \"$COMMAND\" | grep -qE '^\\s*git\\s+clean\\s+-[a-z]*[fd]|;\\s*git\\s+clean|&&\\s*git\\s+clean|\\|\\|\\s*git\\s+clean'; then\n echo \"BLOCKED: git clean removes untracked files permanently.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git clean -n (dry run) first to see what would be deleted.\" >&2\n exit 2\nfi\n\n# --- Check 4: chmod 777 on broad paths ---\nif echo \"$COMMAND\" | grep -qE 'chmod\\s+(-R\\s+)?777\\s+(\\/|~|\\.)'; then\n echo \"BLOCKED: chmod 777 on broad path is a security risk.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n exit 2\nfi\n\n# --- Check 5: find -delete on broad patterns ---\nif echo \"$COMMAND\" | grep -qE 'find\\s+(\\/|~|\\.\\.)\\s.*-delete'; then\n echo \"BLOCKED: find -delete on broad path risks mass deletion.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: find ... -print first to verify what matches.\" >&2\n exit 2\nfi\n\nexit 0\n",
3
+ "branch-guard": "#!/bin/bash\n# ================================================================\n# branch-guard.sh \u2014 Main/Master Branch Push Blocker\n# ================================================================\n# PURPOSE:\n# Prevents accidental git push commands to main or master branches\n# without explicit approval. Allows pushes to feature/staging branches.\n#\n# Protects production branches from unintended force-pushes,\n# incomplete changes, or unauthorized modifications.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git push origin main\n# - git push --force origin master\n# - Any variant targeting main/master\n#\n# WHAT IT ALLOWS (exit 0):\n# - git push origin feature-branch\n# - git push origin develop\n# - All other git commands\n# - All non-git commands\n#\n# CONFIGURATION:\n# CC_PROTECT_BRANCHES \u2014 colon-separated list of protected branches\n# default: \"main:master\"\n# Example: \"main:master:production\" to add more\n#\n# NOTE: This hook exits 2 (not 1) to distinguish from errors.\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty')\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only check git push commands\nif ! echo \"$COMMAND\" | grep -qE '^\\s*git\\s+push'; then\n exit 0\nfi\n\n# Get protected branches from env, default to main and master\nPROTECTED=\"${CC_PROTECT_BRANCHES:-main:master}\"\n\n# Check if any protected branch appears in the push command\nBLOCKED=0\nIFS=':' read -ra BRANCHES <<< \"$PROTECTED\"\nfor branch in \"${BRANCHES[@]}\"; do\n # Match whole word branch names (main, not mainline)\n if echo \"$COMMAND\" | grep -qwE \"origin\\s+${branch}|${branch}\\s|${branch}$\"; then\n BLOCKED=1\n break\n fi\ndone\n\nif (( BLOCKED == 1 )); then\n echo \"BLOCKED: Attempted push to protected branch.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Protected branches: $PROTECTED\" >&2\n echo \"\" >&2\n echo \"This is likely unintended. Push to a feature/staging branch first,\" >&2\n echo \"then create a pull request for manual review.\" >&2\n exit 2\nfi\n\nexit 0\n",
4
+ "syntax-check": "#!/bin/bash\n# ================================================================\n# syntax-check.sh \u2014 Automatic Syntax Validation After Edits\n# ================================================================\n# PURPOSE:\n# Runs syntax checks immediately after Claude Code edits or\n# writes a file. Catches syntax errors before they propagate\n# into downstream failures.\n#\n# SUPPORTED LANGUAGES:\n# .py \u2014 python -m py_compile\n# .sh \u2014 bash -n\n# .bash \u2014 bash -n\n# .json \u2014 jq empty\n# .yaml \u2014 python3 yaml.safe_load (if PyYAML installed)\n# .yml \u2014 python3 yaml.safe_load (if PyYAML installed)\n# .js \u2014 node --check (if node installed)\n# .ts \u2014 npx tsc --noEmit (if tsc available) [EXPERIMENTAL]\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"Edit|Write\"\n#\n# DESIGN PHILOSOPHY:\n# - Never blocks (always exit 0) \u2014 reports errors but doesn't\n# prevent the edit from completing\n# - Silent on success \u2014 only speaks up when something is wrong\n# - Fails open \u2014 if a checker isn't installed, silently skips\n#\n# BORN FROM:\n# Countless sessions where Claude Code introduced a syntax error,\n# continued working for 10+ tool calls, then hit a wall when\n# trying to run the broken file. Catching it immediately saves\n# context window and frustration.\n# ================================================================\n\nINPUT=$(cat)\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty')\n\n# No file path = nothing to check\nif [[ -z \"$FILE_PATH\" || ! -f \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\nEXT=\"${FILE_PATH##*.}\"\n\ncase \"$EXT\" in\n py)\n if python3 -m py_compile \"$FILE_PATH\" 2>&1; then\n : # silent on success\n else\n echo \"SYNTAX ERROR (Python): $FILE_PATH\" >&2\n fi\n ;;\n sh|bash)\n if bash -n \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (Shell): $FILE_PATH\" >&2\n fi\n ;;\n json)\n if command -v jq &>/dev/null; then\n if jq empty \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (JSON): $FILE_PATH\" >&2\n fi\n fi\n ;;\n yaml|yml)\n if python3 -c \"import yaml\" 2>/dev/null; then\n if python3 -c \"\nimport yaml, sys\nwith open(sys.argv[1]) as f:\n yaml.safe_load(f)\n\" \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (YAML): $FILE_PATH\" >&2\n fi\n fi\n ;;\n js)\n if command -v node &>/dev/null; then\n if node --check \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (JavaScript): $FILE_PATH\" >&2\n fi\n fi\n ;;\n ts)\n # EXPERIMENTAL: TypeScript check requires tsc in PATH\n if command -v npx &>/dev/null; then\n if npx tsc --noEmit \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (TypeScript) [experimental]: $FILE_PATH\" >&2\n fi\n fi\n ;;\n *)\n # Unknown extension \u2014 skip silently\n ;;\nesac\n\nexit 0\n",
5
+ "context-monitor": "#!/bin/bash\n# ================================================================\n# context-monitor.sh \u2014 Context Window Remaining Capacity Monitor\n# ================================================================\n# PURPOSE:\n# Monitors how much context window remains during a Claude Code\n# session. Issues graduated warnings (CAUTION \u2192 WARNING \u2192 CRITICAL\n# \u2192 EMERGENCY) so you never get killed by context exhaustion.\n#\n# HOW IT WORKS:\n# 1. Reads Claude Code's debug log to extract actual token usage\n# 2. Falls back to tool-call-count estimation when debug logs\n# are unavailable\n# 3. Saves current % to /tmp/cc-context-pct (other scripts can\n# read this)\n# 4. At CRITICAL/EMERGENCY, writes an evacuation template to\n# your mission file so you can hand off state before /compact\n#\n# TRIGGER: PostToolUse (all tools)\n# MATCHER: \"\" (empty = every tool invocation)\n#\n# CONFIGURATION:\n# CC_CONTEXT_MISSION_FILE \u2014 path to your mission/state file\n# default: $HOME/mission.md\n#\n# THRESHOLDS (edit below to taste):\n# CAUTION = 40% \u2014 be mindful of consumption\n# WARNING = 25% \u2014 finish current task, save state\n# CRITICAL = 20% \u2014 run /compact immediately\n# EMERGENCY = 15% \u2014 stop everything, evacuate\n#\n# BORN FROM:\n# A session that hit 3% context remaining with no warning.\n# The agent died mid-task and all in-flight work was lost.\n# Never again.\n# ================================================================\n\nSTATE_FILE=\"/tmp/cc-context-state\"\nPCT_FILE=\"/tmp/cc-context-pct\"\nCOUNTER_FILE=\"/tmp/cc-context-monitor-count\"\nMISSION_FILE=\"${CC_CONTEXT_MISSION_FILE:-$HOME/mission.md}\"\n\n# Tool invocation counter (fallback estimator)\nCOUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo 0)\nCOUNT=$((COUNT + 1))\necho \"$COUNT\" > \"$COUNTER_FILE\"\n\n# Check every 3rd invocation to reduce overhead\n# (but always check in CRITICAL/EMERGENCY state)\nLAST_STATE=$(cat \"$STATE_FILE\" 2>/dev/null || echo \"normal\")\nif [ $((COUNT % 3)) -ne 0 ] && [ \"$LAST_STATE\" != \"critical\" ] && [ \"$LAST_STATE\" != \"emergency\" ]; then\n exit 0\nfi\n\n# --- Extract context % from Claude Code debug logs ---\nget_context_pct() {\n local debug_dir=\"$HOME/.claude/debug\"\n if [ ! -d \"$debug_dir\" ]; then\n echo \"\"\n return\n fi\n\n local latest\n latest=$(find \"$debug_dir\" -maxdepth 1 -name '*.txt' -printf '%T@ %p\\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2)\n if [ -z \"$latest\" ]; then\n echo \"\"\n return\n fi\n\n # Parse the last autocompact entry for token counts\n local line\n line=$(grep 'autocompact:' \"$latest\" 2>/dev/null | tail -1)\n if [ -z \"$line\" ]; then\n echo \"\"\n return\n fi\n\n local tokens window\n tokens=$(echo \"$line\" | sed 's/.*tokens=\\([0-9]*\\).*/\\1/')\n window=$(echo \"$line\" | sed 's/.*effectiveWindow=\\([0-9]*\\).*/\\1/')\n\n if [ -n \"$tokens\" ] && [ -n \"$window\" ] && [ \"$window\" -gt 0 ] 2>/dev/null; then\n local pct\n pct=$(( (window - tokens) * 100 / window ))\n echo \"$pct\"\n else\n echo \"\"\n fi\n}\n\nCONTEXT_PCT=$(get_context_pct)\n\n# Fallback: estimate from tool call count when debug logs unavailable\n# Assumes ~180 tool calls fills ~100% of context (conservative)\nif [ -z \"$CONTEXT_PCT\" ]; then\n CONTEXT_PCT=$(( 100 - (COUNT * 100 / 180) ))\n if [ \"$CONTEXT_PCT\" -lt 0 ]; then CONTEXT_PCT=0; fi\n SOURCE=\"estimate\"\nelse\n SOURCE=\"debug\"\nfi\n\necho \"$CONTEXT_PCT\" > \"$PCT_FILE\"\n\nTIMESTAMP=$(date '+%Y-%m-%d %H:%M')\n\n# --- Evacuation template (with cooldown to prevent spam) ---\nEVAC_COOLDOWN_FILE=\"/tmp/cc-context-evac-last\"\nEVAC_COOLDOWN_SEC=1800 # 30 min cooldown between template generations\n\ngenerate_evacuation_template() {\n local level=\"$1\"\n\n # Cooldown check\n if [ -f \"$EVAC_COOLDOWN_FILE\" ]; then\n local last_ts now_ts diff\n last_ts=$(cat \"$EVAC_COOLDOWN_FILE\" 2>/dev/null || echo 0)\n now_ts=$(date +%s)\n diff=$((now_ts - last_ts))\n if [ \"$diff\" -lt \"$EVAC_COOLDOWN_SEC\" ]; then\n return\n fi\n fi\n\n # Don't add a new template if there's already an unfilled one\n if [ -f \"$MISSION_FILE\" ] && grep -q '\\[TODO\\]' \"$MISSION_FILE\" 2>/dev/null; then\n return\n fi\n\n date +%s > \"$EVAC_COOLDOWN_FILE\"\n\n # Create mission file directory if needed\n mkdir -p \"$(dirname \"$MISSION_FILE\")\"\n\n cat >> \"$MISSION_FILE\" << EVAC_EOF\n\n## Context Evacuation Template (${level} - ${TIMESTAMP})\n<!-- Auto-generated by context-monitor.sh. Fill in before /compact -->\n### Current Task\n- Task: [TODO]\n- Progress: [TODO]\n- Files being edited: [TODO]\n\n### Git State\n- Branch: [TODO]\n- Uncommitted changes: [TODO]\n\n### Next Action\n- Next command/action: [TODO]\nEVAC_EOF\n}\n\n# --- Graduated warnings ---\nif [ \"$CONTEXT_PCT\" -le 15 ]; then\n # EMERGENCY\n if [ \"$LAST_STATE\" != \"emergency\" ]; then\n echo \"emergency\" > \"$STATE_FILE\"\n generate_evacuation_template \"EMERGENCY\"\n fi\n echo \"\"\n echo \"EMERGENCY: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Fill in the [TODO] fields in the template\"\n echo \"2. Run /compact\"\n echo \"3. If needed, restart and resume from mission file\"\n echo \"No further work allowed. Evacuate only.\"\n\nelif [ \"$CONTEXT_PCT\" -le 20 ]; then\n # CRITICAL\n if [ \"$LAST_STATE\" != \"critical\" ]; then\n echo \"critical\" > \"$STATE_FILE\"\n generate_evacuation_template \"CRITICAL\"\n fi\n echo \"\"\n echo \"CRITICAL: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Save current task state to the template\"\n echo \"2. Run /compact\"\n\nelif [ \"$CONTEXT_PCT\" -le 25 ]; then\n # WARNING\n if [ \"$LAST_STATE\" != \"warning\" ]; then\n echo \"warning\" > \"$STATE_FILE\"\n echo \"\"\n echo \"WARNING: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Do not start new large tasks. Finish current work and save state.\"\n fi\n\nelif [ \"$CONTEXT_PCT\" -le 40 ]; then\n # CAUTION\n if [ \"$LAST_STATE\" != \"caution\" ]; then\n echo \"caution\" > \"$STATE_FILE\"\n echo \"\"\n echo \"CAUTION: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Be mindful of context consumption. Keep interactions concise.\"\n fi\nfi\n\nexit 0\n"
6
+ }