cc-safe-setup 28.4.0 → 28.4.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 +3 -3
- package/examples/claudemd-enforcer.sh +42 -0
- package/examples/mcp-tool-guard.sh +26 -0
- package/examples/prompt-injection-guard.sh +22 -0
- package/examples/test-before-commit.sh +17 -0
- package/examples/usage-warn.sh +13 -0
- package/index.mjs +450 -65
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
**One command to make Claude Code safe for autonomous operation.** [日本語](docs/README.ja.md)
|
|
8
8
|
|
|
9
|
-
8 built-in + 319 examples = **327 hooks**. 45 CLI commands. 955 tests. 5 languages. [**Getting Started**](https://yurukusa.github.io/cc-safe-setup/getting-started.html) · [**Hub**](https://yurukusa.github.io/cc-safe-setup/hub.html) · [**Recipes**](https://yurukusa.github.io/cc-safe-setup/recipes.html) · [Wizard](https://yurukusa.github.io/cc-safe-setup/wizard.html) · [Cheat Sheet](https://yurukusa.github.io/cc-safe-setup/hooks-cheatsheet.html) · [Builder](https://yurukusa.github.io/cc-safe-setup/builder.html) · [FAQ](https://yurukusa.github.io/cc-safe-setup/faq.html) · [Examples](https://yurukusa.github.io/cc-safe-setup/by-example.html) · [Matrix](https://yurukusa.github.io/cc-safe-setup/matrix.html) · [Playground](https://yurukusa.github.io/cc-hook-registry/playground.html)
|
|
10
|
-
|
|
11
9
|
```bash
|
|
12
10
|
npx cc-safe-setup
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
Installs 8
|
|
13
|
+
Installs 8 safety hooks in ~10 seconds. Blocks `rm -rf /`, prevents pushes to main, catches secret leaks, validates syntax after every edit. Zero dependencies.
|
|
14
|
+
|
|
15
|
+
[**Getting Started**](https://yurukusa.github.io/cc-safe-setup/getting-started.html) · [**All Tools**](https://yurukusa.github.io/cc-safe-setup/hub.html) · [**Recipes**](https://yurukusa.github.io/cc-safe-setup/recipes.html) · [Validate your settings.json](https://yurukusa.github.io/cc-safe-setup/validator.html)
|
|
16
16
|
|
|
17
17
|
```
|
|
18
18
|
cc-safe-setup
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
3
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
4
|
+
RESULT=$(echo "$INPUT" | jq -r '.tool_result // empty' 2>/dev/null)
|
|
5
|
+
[ -z "$COMMAND" ] && exit 0
|
|
6
|
+
if [ "${CC_REQUIRE_TESTS:-0}" = "1" ]; then
|
|
7
|
+
if echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
|
|
8
|
+
LAST_TEST=$(stat -c %Y coverage/.last-run.json 2>/dev/null || stat -c %Y test-results 2>/dev/null || echo 0)
|
|
9
|
+
NOW=$(date +%s)
|
|
10
|
+
if [ "$LAST_TEST" -eq 0 ] || [ $((NOW - LAST_TEST)) -gt 600 ]; then
|
|
11
|
+
echo "⚠ CLAUDE.md VIOLATION: Committed without running tests first" >&2
|
|
12
|
+
fi
|
|
13
|
+
fi
|
|
14
|
+
fi
|
|
15
|
+
if [ -n "${CC_ENFORCED_BRANCH}" ]; then
|
|
16
|
+
if echo "$COMMAND" | grep -qE "git\s+push.*${CC_ENFORCED_BRANCH}"; then
|
|
17
|
+
echo "⚠ CLAUDE.md VIOLATION: Pushed to protected branch '${CC_ENFORCED_BRANCH}'" >&2
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
if [ "${CC_NO_FORCE_PUSH:-1}" = "1" ]; then
|
|
21
|
+
if echo "$COMMAND" | grep -qE 'git\s+push.*--force'; then
|
|
22
|
+
echo "⚠ CLAUDE.md VIOLATION: Force push detected" >&2
|
|
23
|
+
fi
|
|
24
|
+
fi
|
|
25
|
+
MAX_FILES=${CC_MAX_FILES_PER_COMMIT:-20}
|
|
26
|
+
if echo "$COMMAND" | grep -qE '^\s*git\s+commit'; then
|
|
27
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l)
|
|
28
|
+
if [ "$STAGED" -gt "$MAX_FILES" ]; then
|
|
29
|
+
echo "⚠ CLAUDE.md VIOLATION: Committing $STAGED files (max: $MAX_FILES)" >&2
|
|
30
|
+
fi
|
|
31
|
+
fi
|
|
32
|
+
if echo "$COMMAND" | grep -qE '^\s*git\s+add'; then
|
|
33
|
+
STAGED_FILES=$(git diff --cached --name-only 2>/dev/null)
|
|
34
|
+
for f in $STAGED_FILES; do
|
|
35
|
+
[ -f "$f" ] || continue
|
|
36
|
+
if grep -qE 'console\.log\(|debugger;|print\(' "$f" 2>/dev/null; then
|
|
37
|
+
echo "⚠ CLAUDE.md REMINDER: Debug statements found in staged file: $f" >&2
|
|
38
|
+
break
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
fi
|
|
42
|
+
exit 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
INPUT=$(cat)
|
|
2
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
3
|
+
echo "$TOOL" | grep -q '^mcp__' || exit 0
|
|
4
|
+
if [ "${CC_MCP_WARN_ALL:-0}" = "1" ]; then
|
|
5
|
+
echo "NOTE: MCP tool call: $TOOL" >&2
|
|
6
|
+
fi
|
|
7
|
+
BLOCKED="${CC_MCP_BLOCKED_TOOLS:-}"
|
|
8
|
+
if [ -n "$BLOCKED" ]; then
|
|
9
|
+
IFS=',' read -ra PATTERNS <<< "$BLOCKED"
|
|
10
|
+
for pattern in "${PATTERNS[@]}"; do
|
|
11
|
+
pattern=$(echo "$pattern" | xargs) # trim whitespace
|
|
12
|
+
if [[ "$TOOL" == *"$pattern"* ]]; then
|
|
13
|
+
echo "BLOCKED: MCP tool $TOOL matches blocked pattern: $pattern" >&2
|
|
14
|
+
exit 2
|
|
15
|
+
fi
|
|
16
|
+
done
|
|
17
|
+
fi
|
|
18
|
+
case "$TOOL" in
|
|
19
|
+
*delete*|*remove*|*drop*|*destroy*|*purge*)
|
|
20
|
+
echo "WARNING: Potentially destructive MCP tool: $TOOL" >&2
|
|
21
|
+
;;
|
|
22
|
+
*send_email*|*send_message*|*post*|*publish*)
|
|
23
|
+
echo "WARNING: MCP tool with external side effects: $TOOL" >&2
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
exit 0
|
|
@@ -44,6 +44,28 @@ if echo "$OUTPUT" | grep -qP '<!--.*(?:execute|run|delete|remove).*-->'; then
|
|
|
44
44
|
SUSPICIOUS=1
|
|
45
45
|
fi
|
|
46
46
|
|
|
47
|
+
# tool_runtime_configuration injection (GitHub #28586)
|
|
48
|
+
if echo "$OUTPUT" | grep -qiE '<tool_runtime_configuration>|</tool_runtime_configuration>'; then
|
|
49
|
+
echo "WARNING: tool_runtime_configuration injection detected — can disable tools" >&2
|
|
50
|
+
SUSPICIOUS=1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# MCP server instruction override (GitHub #30545)
|
|
54
|
+
if echo "$OUTPUT" | grep -qiE 'override.*CLAUDE\.md|ignore.*project\s+rules|disregard.*instructions'; then
|
|
55
|
+
echo "WARNING: Possible MCP instruction override detected" >&2
|
|
56
|
+
SUSPICIOUS=1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Base64-encoded commands (obfuscated injection)
|
|
60
|
+
if echo "$OUTPUT" | grep -qE '[A-Za-z0-9+/]{40,}={0,2}'; then
|
|
61
|
+
# Check if it decodes to something suspicious
|
|
62
|
+
DECODED=$(echo "$OUTPUT" | grep -oE '[A-Za-z0-9+/]{40,}={0,2}' | head -1 | base64 -d 2>/dev/null || true)
|
|
63
|
+
if echo "$DECODED" | grep -qiE 'rm\s+-rf|curl.*\|.*sh|eval|exec'; then
|
|
64
|
+
echo "WARNING: Base64-encoded command detected in tool output" >&2
|
|
65
|
+
SUSPICIOUS=1
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
|
|
47
69
|
if [ "$SUSPICIOUS" -eq 1 ]; then
|
|
48
70
|
echo "Review the output carefully before acting on it." >&2
|
|
49
71
|
fi
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
COMMAND=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
2
|
+
[ -z "$COMMAND" ] && exit 0
|
|
3
|
+
echo "$COMMAND" | grep -qE '^\s*git\s+commit' || exit 0
|
|
4
|
+
RECENT=0
|
|
5
|
+
TIMEOUT=${CC_TEST_TIMEOUT:-600}
|
|
6
|
+
NOW=$(date +%s)
|
|
7
|
+
for marker in coverage/.last-run.json test-results .nyc_output junit.xml; do
|
|
8
|
+
[ -e "$marker" ] || continue
|
|
9
|
+
MTIME=$(stat -c %Y "$marker" 2>/dev/null || echo 0)
|
|
10
|
+
[ $((NOW - MTIME)) -lt "$TIMEOUT" ] && RECENT=1 && break
|
|
11
|
+
done
|
|
12
|
+
if [ "$RECENT" -eq 0 ]; then
|
|
13
|
+
echo "BLOCKED: No recent test results (within $((TIMEOUT/60)) min)" >&2
|
|
14
|
+
echo "Run your test suite first, then commit." >&2
|
|
15
|
+
exit 2
|
|
16
|
+
fi
|
|
17
|
+
exit 0
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
COUNTER="${HOME}/.claude/session-tool-count"
|
|
2
|
+
COUNT=$(cat "$COUNTER" 2>/dev/null || echo 0)
|
|
3
|
+
COUNT=$((COUNT + 1))
|
|
4
|
+
echo "$COUNT" > "$COUNTER"
|
|
5
|
+
WARN1=${CC_USAGE_WARN1:-100}
|
|
6
|
+
WARN2=${CC_USAGE_WARN2:-200}
|
|
7
|
+
WARN3=${CC_USAGE_WARN3:-300}
|
|
8
|
+
case "$COUNT" in
|
|
9
|
+
"$WARN1") echo "NOTE: $COUNT tool calls this session" >&2 ;;
|
|
10
|
+
"$WARN2") echo "WARNING: $COUNT tool calls — consider wrapping up" >&2 ;;
|
|
11
|
+
"$WARN3") echo "ALERT: $COUNT tool calls — approaching typical session limit" >&2 ;;
|
|
12
|
+
esac
|
|
13
|
+
exit 0
|
package/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync } from 'fs';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, copyFileSync, unlinkSync } from 'fs';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { createInterface } from 'readline';
|
|
@@ -11,6 +11,9 @@ const HOME = homedir();
|
|
|
11
11
|
const HOOKS_DIR = join(HOME, '.claude', 'hooks');
|
|
12
12
|
const SETTINGS_PATH = join(HOME, '.claude', 'settings.json');
|
|
13
13
|
|
|
14
|
+
// Convert Windows backslash paths to bash-compatible forward slashes
|
|
15
|
+
const toBashPath = (p) => p.replace(/\\/g, '/');
|
|
16
|
+
|
|
14
17
|
const c = {
|
|
15
18
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
16
19
|
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
@@ -114,10 +117,15 @@ const SUGGEST = process.argv.includes('--suggest');
|
|
|
114
117
|
const INIT_PROJECT = process.argv.includes('--init-project');
|
|
115
118
|
const SCORE_ONLY = process.argv.includes('--score');
|
|
116
119
|
const CHANGELOG_CMD = process.argv.includes('--changelog');
|
|
120
|
+
const VALIDATE = process.argv.includes('--validate');
|
|
121
|
+
const SAFE_MODE = process.argv.includes('--safe-mode');
|
|
122
|
+
const SAFE_MODE_OFF = process.argv.includes('--safe-mode') && process.argv.includes('off');
|
|
117
123
|
const SIMULATE_IDX = process.argv.findIndex(a => a === '--simulate');
|
|
118
124
|
const SIMULATE_CMD = SIMULATE_IDX !== -1 ? process.argv.slice(SIMULATE_IDX + 1).join(' ') : null;
|
|
119
125
|
const PROTECT_IDX = process.argv.findIndex(a => a === '--protect');
|
|
120
126
|
const PROTECT_PATH = PROTECT_IDX !== -1 ? process.argv[PROTECT_IDX + 1] : null;
|
|
127
|
+
const RULES_IDX = process.argv.findIndex(a => a === '--rules');
|
|
128
|
+
const RULES_FILE = RULES_IDX !== -1 ? (process.argv[RULES_IDX + 1] || '.claude/rules.yaml') : null;
|
|
121
129
|
const TEST_HOOK_IDX = process.argv.findIndex(a => a === '--test-hook');
|
|
122
130
|
const TEST_HOOK = TEST_HOOK_IDX !== -1 ? process.argv[TEST_HOOK_IDX + 1] : null;
|
|
123
131
|
const WHY_IDX = process.argv.findIndex(a => a === '--why');
|
|
@@ -127,51 +135,43 @@ if (HELP) {
|
|
|
127
135
|
console.log(`
|
|
128
136
|
cc-safe-setup — Make Claude Code safe for autonomous operation
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
npx cc-safe-setup Install 8 safety hooks
|
|
132
|
-
npx cc-safe-setup --
|
|
133
|
-
npx cc-safe-setup --
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
npx cc-safe-setup --diff-hooks <path> Compare hooks between two settings files
|
|
168
|
-
npx cc-safe-setup --from-claudemd Convert CLAUDE.md rules into hooks
|
|
169
|
-
npx cc-safe-setup --health Hook health dashboard (size, permissions, age)
|
|
170
|
-
npx cc-safe-setup --migrate-from <tool> Migrate from safety-net/hooks-mastery/etc.
|
|
171
|
-
npx cc-safe-setup --team Set up project-level hooks (commit to repo for team)
|
|
172
|
-
npx cc-safe-setup --profile <level> Switch safety profile (strict/standard/minimal)
|
|
173
|
-
npx cc-safe-setup --analyze Analyze what Claude did in your last session
|
|
174
|
-
npx cc-safe-setup --shield Maximum safety in one command (fix + scan + install + CLAUDE.md)
|
|
138
|
+
Quick Start:
|
|
139
|
+
npx cc-safe-setup Install 8 safety hooks (30 sec)
|
|
140
|
+
npx cc-safe-setup --shield Maximum safety — one command
|
|
141
|
+
npx cc-safe-setup --doctor Diagnose hook problems
|
|
142
|
+
|
|
143
|
+
Protect:
|
|
144
|
+
--protect .env Block edits to a specific file
|
|
145
|
+
--guard "never delete prod" Enforce a rule from plain English
|
|
146
|
+
--simulate "rm -rf /" Preview how hooks react to a command
|
|
147
|
+
--validate Check all hooks for errors (auto-fix)
|
|
148
|
+
--safe-mode Emergency: disable all hooks
|
|
149
|
+
|
|
150
|
+
Install & Configure:
|
|
151
|
+
--install-example <name> Install from 300+ example hooks
|
|
152
|
+
--examples Browse examples by category
|
|
153
|
+
--from-claudemd Convert CLAUDE.md rules into hooks
|
|
154
|
+
--rules [file] Compile YAML rules into hooks
|
|
155
|
+
--team Set up project-level hooks for git
|
|
156
|
+
--profile <level> Switch profile (strict/standard/minimal)
|
|
157
|
+
--shield Full setup: hooks + scan + CLAUDE.md
|
|
158
|
+
|
|
159
|
+
Diagnose & Monitor:
|
|
160
|
+
--status / --verify Check installed hooks / test them
|
|
161
|
+
--doctor 13-point diagnostic
|
|
162
|
+
--audit [--fix] [--json] Safety score 0-100
|
|
163
|
+
--watch Live blocked command feed
|
|
164
|
+
--health Hook health dashboard
|
|
165
|
+
--suggest Predict risks from project analysis
|
|
166
|
+
|
|
167
|
+
Manage:
|
|
168
|
+
--dry-run / --uninstall Preview / remove hooks
|
|
169
|
+
--export / --import Share configuration
|
|
170
|
+
--diff <file> Compare settings
|
|
171
|
+
--lint Static analysis of config
|
|
172
|
+
|
|
173
|
+
More: --create, --why, --replay, --score, --changelog, --analyze,
|
|
174
|
+
--benchmark, --dashboard, --migrate-from, --init-project
|
|
175
175
|
npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
|
|
176
176
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
177
177
|
npx cc-safe-setup --export Export hooks config for team sharing
|
|
@@ -305,6 +305,8 @@ function status() {
|
|
|
305
305
|
console.log(' ' + c.dim + '+ ' + installedExamples.length + ' example hooks' + c.reset);
|
|
306
306
|
}
|
|
307
307
|
console.log();
|
|
308
|
+
console.log(c.dim + ' Tip: --validate to check health · --simulate "cmd" to test · --shield for max safety' + c.reset);
|
|
309
|
+
console.log();
|
|
308
310
|
|
|
309
311
|
// Exit code for CI: 0 = all installed, 1 = missing hooks
|
|
310
312
|
if (missing > 0) process.exit(1);
|
|
@@ -615,7 +617,7 @@ async function installExample(name) {
|
|
|
615
617
|
|
|
616
618
|
const hookEntry = {
|
|
617
619
|
matcher: matcher,
|
|
618
|
-
hooks: [{ type: 'command', command: destPath }],
|
|
620
|
+
hooks: [{ type: 'command', command: toBashPath(destPath) }],
|
|
619
621
|
};
|
|
620
622
|
|
|
621
623
|
// Check if already installed
|
|
@@ -2214,7 +2216,7 @@ async function profile(level) {
|
|
|
2214
2216
|
|
|
2215
2217
|
// Register all hooks in settings
|
|
2216
2218
|
const hookFiles = prof.hooks.filter(h => existsSync(join(HOOKS_DIR, `${h}.sh`)));
|
|
2217
|
-
const bashHooks = hookFiles.map(h => ({ type: 'command', command: `bash ${join(HOOKS_DIR, h + '.sh')}` }));
|
|
2219
|
+
const bashHooks = hookFiles.map(h => ({ type: 'command', command: `bash ${toBashPath(join(HOOKS_DIR, h + '.sh'))}` }));
|
|
2218
2220
|
|
|
2219
2221
|
// Simplified: put all under PreToolUse Bash for now
|
|
2220
2222
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
@@ -3881,7 +3883,7 @@ exit 0`,
|
|
|
3881
3883
|
if (!existing.some(cmd => cmd.includes(matched.name))) {
|
|
3882
3884
|
settings.hooks[matched.trigger].push({
|
|
3883
3885
|
matcher: matched.matcher,
|
|
3884
|
-
hooks: [{ type: 'command', command: hookPath }],
|
|
3886
|
+
hooks: [{ type: 'command', command: toBashPath(hookPath) }],
|
|
3885
3887
|
});
|
|
3886
3888
|
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
3887
3889
|
console.log(c.green + ' ✓ Registered in settings.json' + c.reset);
|
|
@@ -4266,6 +4268,334 @@ async function watch() {
|
|
|
4266
4268
|
}
|
|
4267
4269
|
}
|
|
4268
4270
|
|
|
4271
|
+
async function validateHooks() {
|
|
4272
|
+
const { spawnSync } = await import('child_process');
|
|
4273
|
+
const { readdirSync, renameSync } = await import('fs');
|
|
4274
|
+
|
|
4275
|
+
console.log();
|
|
4276
|
+
console.log(c.bold + ' cc-safe-setup --validate' + c.reset);
|
|
4277
|
+
console.log(c.dim + ' Checking all hooks for syntax errors and dangerous exit codes...' + c.reset);
|
|
4278
|
+
console.log();
|
|
4279
|
+
|
|
4280
|
+
if (!existsSync(HOOKS_DIR)) {
|
|
4281
|
+
console.log(c.yellow + ' No hooks directory found.' + c.reset);
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
const disabledDir = join(HOME, '.claude', 'hooks-disabled');
|
|
4286
|
+
const hooks = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'));
|
|
4287
|
+
let ok = 0, dangerous = 0;
|
|
4288
|
+
|
|
4289
|
+
for (const h of hooks) {
|
|
4290
|
+
const path = join(HOOKS_DIR, h);
|
|
4291
|
+
const name = h.replace('.sh', '');
|
|
4292
|
+
|
|
4293
|
+
// 1. Syntax check
|
|
4294
|
+
const syntax = spawnSync('bash', ['-n', path], { stdio: 'pipe', timeout: 5000 });
|
|
4295
|
+
if (syntax.status !== 0) {
|
|
4296
|
+
mkdirSync(disabledDir, { recursive: true });
|
|
4297
|
+
renameSync(path, join(disabledDir, h));
|
|
4298
|
+
console.log(c.red + ' ✗ ' + name + c.reset + ' — SYNTAX ERROR (exit ' + syntax.status + ')');
|
|
4299
|
+
console.log(c.dim + ' Moved to hooks-disabled/. A syntax error exit 2 blocks ALL tools.' + c.reset);
|
|
4300
|
+
dangerous++;
|
|
4301
|
+
continue;
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
// 2. Empty input test — exit 2 = would block all tools
|
|
4305
|
+
const test = spawnSync('bash', [path], {
|
|
4306
|
+
input: '{}', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
4307
|
+
});
|
|
4308
|
+
if (test.status === 2) {
|
|
4309
|
+
mkdirSync(disabledDir, { recursive: true });
|
|
4310
|
+
renameSync(path, join(disabledDir, h));
|
|
4311
|
+
console.log(c.red + ' ✗ ' + name + c.reset + ' — BLOCKS on empty input (exit 2)');
|
|
4312
|
+
console.log(c.dim + ' Moved to hooks-disabled/.' + c.reset);
|
|
4313
|
+
dangerous++;
|
|
4314
|
+
continue;
|
|
4315
|
+
}
|
|
4316
|
+
|
|
4317
|
+
ok++;
|
|
4318
|
+
console.log(c.green + ' ✓ ' + c.reset + name);
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
console.log();
|
|
4322
|
+
if (dangerous > 0) {
|
|
4323
|
+
console.log(c.red + ' ' + dangerous + ' dangerous hook(s) disabled.' + c.reset);
|
|
4324
|
+
console.log(c.dim + ' Restart Claude Code to apply. Disabled hooks are in ~/.claude/hooks-disabled/' + c.reset);
|
|
4325
|
+
} else {
|
|
4326
|
+
console.log(c.green + ' All ' + ok + ' hooks passed validation.' + c.reset);
|
|
4327
|
+
}
|
|
4328
|
+
console.log();
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
async function safeMode(enable) {
|
|
4332
|
+
const { readdirSync, renameSync, rmdirSync } = await import('fs');
|
|
4333
|
+
const disabledDir = join(HOME, '.claude', 'hooks-disabled');
|
|
4334
|
+
const backupSettings = join(HOME, '.claude', 'settings-backup.json');
|
|
4335
|
+
|
|
4336
|
+
console.log();
|
|
4337
|
+
if (enable) {
|
|
4338
|
+
console.log(c.bold + ' cc-safe-setup --safe-mode' + c.reset);
|
|
4339
|
+
console.log(c.dim + ' Disabling all hooks...' + c.reset);
|
|
4340
|
+
|
|
4341
|
+
if (!existsSync(HOOKS_DIR)) {
|
|
4342
|
+
console.log(c.yellow + ' No hooks to disable.' + c.reset);
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
mkdirSync(disabledDir, { recursive: true });
|
|
4347
|
+
const hooks = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'));
|
|
4348
|
+
for (const h of hooks) {
|
|
4349
|
+
renameSync(join(HOOKS_DIR, h), join(disabledDir, h));
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
// Backup settings and clear hooks
|
|
4353
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
4354
|
+
const settings = readFileSync(SETTINGS_PATH, 'utf-8');
|
|
4355
|
+
writeFileSync(backupSettings, settings);
|
|
4356
|
+
const parsed = JSON.parse(settings);
|
|
4357
|
+
parsed.hooks = {};
|
|
4358
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(parsed, null, 2));
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
console.log(c.green + ' Safe mode ON.' + c.reset + ' ' + hooks.length + ' hooks disabled.');
|
|
4362
|
+
console.log(c.dim + ' Restart Claude Code. Run --safe-mode off to restore.' + c.reset);
|
|
4363
|
+
} else {
|
|
4364
|
+
console.log(c.bold + ' cc-safe-setup --safe-mode off' + c.reset);
|
|
4365
|
+
|
|
4366
|
+
if (existsSync(disabledDir)) {
|
|
4367
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
4368
|
+
const hooks = readdirSync(disabledDir).filter(f => f.endsWith('.sh'));
|
|
4369
|
+
for (const h of hooks) {
|
|
4370
|
+
renameSync(join(disabledDir, h), join(HOOKS_DIR, h));
|
|
4371
|
+
}
|
|
4372
|
+
try { rmdirSync(disabledDir); } catch {}
|
|
4373
|
+
console.log(c.green + ' Restored ' + hooks.length + ' hooks.' + c.reset);
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
if (existsSync(backupSettings)) {
|
|
4377
|
+
copyFileSync(backupSettings, SETTINGS_PATH);
|
|
4378
|
+
unlinkSync(backupSettings);
|
|
4379
|
+
console.log(c.green + ' Restored settings.json from backup.' + c.reset);
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
console.log(c.dim + ' Restart Claude Code to activate hooks.' + c.reset);
|
|
4383
|
+
}
|
|
4384
|
+
console.log();
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
async function compileRules(rulesFile) {
|
|
4388
|
+
console.log();
|
|
4389
|
+
console.log(c.bold + ' cc-safe-setup --rules' + c.reset);
|
|
4390
|
+
|
|
4391
|
+
if (!existsSync(rulesFile)) {
|
|
4392
|
+
// Create example rules file
|
|
4393
|
+
const exampleDir = rulesFile.includes('/') ? rulesFile.substring(0, rulesFile.lastIndexOf('/')) : '.';
|
|
4394
|
+
mkdirSync(exampleDir, { recursive: true });
|
|
4395
|
+
writeFileSync(rulesFile, `# Claude Code Safety Rules
|
|
4396
|
+
# Run: npx cc-safe-setup --rules ${rulesFile}
|
|
4397
|
+
|
|
4398
|
+
# Block dangerous commands
|
|
4399
|
+
- block: "rm -rf on root or home"
|
|
4400
|
+
pattern: "rm\\s+-rf\\s+(\\/$|~)"
|
|
4401
|
+
|
|
4402
|
+
- block: "git push to main"
|
|
4403
|
+
pattern: "git\\s+push.*(main|master)"
|
|
4404
|
+
|
|
4405
|
+
- block: "git force push"
|
|
4406
|
+
pattern: "git\\s+push.*--force"
|
|
4407
|
+
|
|
4408
|
+
- block: "git add .env"
|
|
4409
|
+
pattern: "git\\s+add.*\\.env"
|
|
4410
|
+
|
|
4411
|
+
# Auto-approve safe commands
|
|
4412
|
+
- approve: "read-only commands"
|
|
4413
|
+
commands: [cat, head, tail, ls, grep, find, which, pwd, date]
|
|
4414
|
+
|
|
4415
|
+
- approve: "git read commands"
|
|
4416
|
+
pattern: "^\\s*git\\s+(status|log|diff|show|branch)"
|
|
4417
|
+
|
|
4418
|
+
- approve: "test runners"
|
|
4419
|
+
pattern: "^\\s*(npm\\s+test|pytest|go\\s+test|cargo\\s+test)"
|
|
4420
|
+
|
|
4421
|
+
# Protect files from edits
|
|
4422
|
+
- protect: ".env"
|
|
4423
|
+
- protect: "config/secrets.yaml"
|
|
4424
|
+
`);
|
|
4425
|
+
console.log(c.green + ' + ' + c.reset + 'Created ' + rulesFile + ' with example rules');
|
|
4426
|
+
console.log(c.dim + ' Edit the file, then run this command again to compile.' + c.reset);
|
|
4427
|
+
console.log();
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
// Parse YAML (simple parser — no dependency needed)
|
|
4432
|
+
const content = readFileSync(rulesFile, 'utf-8');
|
|
4433
|
+
const rules = [];
|
|
4434
|
+
let current = null;
|
|
4435
|
+
|
|
4436
|
+
for (const line of content.split('\n')) {
|
|
4437
|
+
const trimmed = line.trim();
|
|
4438
|
+
if (trimmed.startsWith('#') || !trimmed) continue;
|
|
4439
|
+
|
|
4440
|
+
const blockMatch = trimmed.match(/^-\s+block:\s+"(.+)"/);
|
|
4441
|
+
const approveMatch = trimmed.match(/^-\s+approve:\s+"(.+)"/);
|
|
4442
|
+
const protectMatch = trimmed.match(/^-\s+protect:\s+"(.+)"/);
|
|
4443
|
+
const patternMatch = trimmed.match(/^pattern:\s+"(.+)"/);
|
|
4444
|
+
const commandsMatch = trimmed.match(/^commands:\s+\[(.+)\]/);
|
|
4445
|
+
|
|
4446
|
+
if (blockMatch) { current = { type: 'block', name: blockMatch[1] }; rules.push(current); }
|
|
4447
|
+
else if (approveMatch) { current = { type: 'approve', name: approveMatch[1] }; rules.push(current); }
|
|
4448
|
+
else if (protectMatch) { rules.push({ type: 'protect', path: protectMatch[1] }); current = null; }
|
|
4449
|
+
else if (patternMatch && current) { current.pattern = patternMatch[1]; }
|
|
4450
|
+
else if (commandsMatch && current) { current.commands = commandsMatch[1].split(',').map(s => s.trim()); }
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
if (rules.length === 0) {
|
|
4454
|
+
console.log(c.yellow + ' No rules found in ' + rulesFile + c.reset);
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
console.log(c.dim + ' Compiling ' + rules.length + ' rules from ' + rulesFile + c.reset);
|
|
4459
|
+
|
|
4460
|
+
// Generate consolidated hook script
|
|
4461
|
+
let script = `#!/bin/bash
|
|
4462
|
+
# Auto-generated from: ${rulesFile}
|
|
4463
|
+
# Generated: $(date -Iseconds)
|
|
4464
|
+
# Rules: ${rules.length}
|
|
4465
|
+
# Regenerate: npx cc-safe-setup --rules ${rulesFile}
|
|
4466
|
+
|
|
4467
|
+
INPUT=$(cat)
|
|
4468
|
+
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
4469
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
4470
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
4471
|
+
|
|
4472
|
+
`;
|
|
4473
|
+
|
|
4474
|
+
// Block rules
|
|
4475
|
+
const blocks = rules.filter(r => r.type === 'block');
|
|
4476
|
+
if (blocks.length > 0) {
|
|
4477
|
+
script += '# === Block Rules ===\n';
|
|
4478
|
+
script += 'if [ "$TOOL" = "Bash" ] && [ -n "$COMMAND" ]; then\n';
|
|
4479
|
+
for (const rule of blocks) {
|
|
4480
|
+
if (rule.pattern) {
|
|
4481
|
+
script += ` if echo "$COMMAND" | grep -qE '${rule.pattern}'; then\n`;
|
|
4482
|
+
script += ` echo "BLOCKED: ${rule.name}" >&2\n`;
|
|
4483
|
+
script += ` exit 2\n`;
|
|
4484
|
+
script += ` fi\n`;
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
script += 'fi\n\n';
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
// Approve rules
|
|
4491
|
+
const approves = rules.filter(r => r.type === 'approve');
|
|
4492
|
+
if (approves.length > 0) {
|
|
4493
|
+
script += '# === Approve Rules ===\n';
|
|
4494
|
+
script += 'if [ "$TOOL" = "Bash" ] && [ -n "$COMMAND" ]; then\n';
|
|
4495
|
+
for (const rule of approves) {
|
|
4496
|
+
if (rule.commands) {
|
|
4497
|
+
const base = '$(echo "$COMMAND" | awk \'{ print $1 }\' | sed \'s|.*/||\')'
|
|
4498
|
+
script += ` BASE=${base}\n`;
|
|
4499
|
+
script += ` case "$BASE" in\n`;
|
|
4500
|
+
script += ` ${rule.commands.join('|')}) echo '{"decision":"approve","reason":"${rule.name}"}'; exit 0 ;;\n`;
|
|
4501
|
+
script += ` esac\n`;
|
|
4502
|
+
}
|
|
4503
|
+
if (rule.pattern) {
|
|
4504
|
+
script += ` if echo "$COMMAND" | grep -qE '${rule.pattern}'; then\n`;
|
|
4505
|
+
script += ` echo '{"decision":"approve","reason":"${rule.name}"}'\n`;
|
|
4506
|
+
script += ` exit 0\n`;
|
|
4507
|
+
script += ` fi\n`;
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
script += 'fi\n\n';
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
// Protect rules
|
|
4514
|
+
const protects = rules.filter(r => r.type === 'protect');
|
|
4515
|
+
if (protects.length > 0) {
|
|
4516
|
+
script += '# === Protect Rules ===\n';
|
|
4517
|
+
script += 'if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then\n';
|
|
4518
|
+
script += ' [ -z "$FILE" ] && exit 0\n';
|
|
4519
|
+
for (const rule of protects) {
|
|
4520
|
+
const path = rule.path;
|
|
4521
|
+
if (path.endsWith('/')) {
|
|
4522
|
+
script += ` [[ "$FILE" == *"${path}"* ]] && { jq -n '{"decision":"block","reason":"Protected: ${path}"}'; exit 0; }\n`;
|
|
4523
|
+
} else {
|
|
4524
|
+
script += ` [[ "$FILE" == *"${path}" ]] && { jq -n '{"decision":"block","reason":"Protected: ${path}"}'; exit 0; }\n`;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
script += 'fi\n\n';
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
script += 'exit 0\n';
|
|
4531
|
+
|
|
4532
|
+
// Write hook
|
|
4533
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
4534
|
+
const hookPath = join(HOOKS_DIR, 'compiled-rules.sh');
|
|
4535
|
+
writeFileSync(hookPath, script);
|
|
4536
|
+
chmodSync(hookPath, 0o755);
|
|
4537
|
+
|
|
4538
|
+
// Syntax check — a broken hook returns exit 2 which blocks ALL tools
|
|
4539
|
+
const { execSync } = await import('child_process');
|
|
4540
|
+
try {
|
|
4541
|
+
execSync(`bash -n "${hookPath}"`, { stdio: 'pipe' });
|
|
4542
|
+
} catch (e) {
|
|
4543
|
+
const stderr = (e.stderr || '').toString().trim();
|
|
4544
|
+
unlinkSync(hookPath);
|
|
4545
|
+
console.log(c.red + ' ✗ Syntax error in generated hook — aborted' + c.reset);
|
|
4546
|
+
if (stderr) console.log(c.dim + ' ' + stderr + c.reset);
|
|
4547
|
+
console.log(c.dim + ' Check your rules file for unescaped special characters.' + c.reset);
|
|
4548
|
+
console.log();
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
// Register in settings.json
|
|
4553
|
+
let settings = {};
|
|
4554
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
4555
|
+
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
4556
|
+
}
|
|
4557
|
+
if (!settings.hooks) settings.hooks = {};
|
|
4558
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
4559
|
+
|
|
4560
|
+
const hookCmd = `bash ${hookPath}`;
|
|
4561
|
+
|
|
4562
|
+
// Check all matchers for existing compiled-rules entry
|
|
4563
|
+
let found = false;
|
|
4564
|
+
for (const entry of settings.hooks.PreToolUse) {
|
|
4565
|
+
const idx = (entry.hooks || []).findIndex(h => h.command && h.command.includes('compiled-rules'));
|
|
4566
|
+
if (idx !== -1) {
|
|
4567
|
+
entry.hooks[idx] = { type: 'command', command: hookCmd };
|
|
4568
|
+
found = true;
|
|
4569
|
+
break;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
if (!found) {
|
|
4574
|
+
// Add to catch-all matcher
|
|
4575
|
+
let allMatcher = settings.hooks.PreToolUse.find(e => e.matcher === '');
|
|
4576
|
+
if (!allMatcher) {
|
|
4577
|
+
allMatcher = { matcher: '', hooks: [] };
|
|
4578
|
+
settings.hooks.PreToolUse.push(allMatcher);
|
|
4579
|
+
}
|
|
4580
|
+
allMatcher.hooks.push({ type: 'command', command: hookCmd });
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
4584
|
+
|
|
4585
|
+
// Summary
|
|
4586
|
+
console.log();
|
|
4587
|
+
for (const rule of rules) {
|
|
4588
|
+
if (rule.type === 'block') console.log(c.red + ' ✗ ' + c.reset + 'Block: ' + rule.name);
|
|
4589
|
+
else if (rule.type === 'approve') console.log(c.green + ' ✓ ' + c.reset + 'Approve: ' + rule.name);
|
|
4590
|
+
else if (rule.type === 'protect') console.log(c.blue + ' 🔒 ' + c.reset + 'Protect: ' + rule.path);
|
|
4591
|
+
}
|
|
4592
|
+
console.log();
|
|
4593
|
+
console.log(c.bold + ' ' + rules.length + ' rules compiled' + c.reset + ' → ' + hookPath);
|
|
4594
|
+
console.log(c.dim + ' Restart Claude Code to activate.' + c.reset);
|
|
4595
|
+
console.log(c.dim + ' Edit ' + rulesFile + ' and re-run to update.' + c.reset);
|
|
4596
|
+
console.log();
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4269
4599
|
async function protect(targetPath) {
|
|
4270
4600
|
const { resolve, basename } = await import('path');
|
|
4271
4601
|
|
|
@@ -4303,13 +4633,44 @@ fi
|
|
|
4303
4633
|
exit 0
|
|
4304
4634
|
`;
|
|
4305
4635
|
|
|
4306
|
-
//
|
|
4636
|
+
// Safety: write to /tmp first, validate, then copy to hooks/
|
|
4637
|
+
const tmpScript = '/tmp/cc-compiled-rules-' + Date.now() + '.sh';
|
|
4638
|
+
writeFileSync(tmpScript, script);
|
|
4639
|
+
chmodSync(tmpScript, 0o755);
|
|
4640
|
+
|
|
4641
|
+
// Validate 1: bash syntax check
|
|
4642
|
+
const { spawnSync: checkSpawn } = await import('child_process');
|
|
4643
|
+
const syntaxCheck = checkSpawn('bash', ['-n', tmpScript], { stdio: 'pipe' });
|
|
4644
|
+
if (syntaxCheck.status !== 0) {
|
|
4645
|
+
const err = (syntaxCheck.stderr || '').toString().trim();
|
|
4646
|
+
console.log(c.red + ' ERROR: Generated script has syntax errors:' + c.reset);
|
|
4647
|
+
console.log(c.dim + ' ' + err + c.reset);
|
|
4648
|
+
try { unlinkSync(tmpScript); } catch {}
|
|
4649
|
+
console.log(c.dim + ' Script NOT installed. Fix your rules and try again.' + c.reset);
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
// Validate 2: empty input must NOT return exit 2 (which blocks all tools)
|
|
4654
|
+
const emptyTest = checkSpawn('bash', [tmpScript], {
|
|
4655
|
+
input: '{}', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
4656
|
+
});
|
|
4657
|
+
if (emptyTest.status === 2) {
|
|
4658
|
+
console.log(c.red + ' ERROR: Script returns exit 2 on empty input.' + c.reset);
|
|
4659
|
+
console.log(c.red + ' This would BLOCK ALL Claude Code tools.' + c.reset);
|
|
4660
|
+
try { unlinkSync(tmpScript); } catch {}
|
|
4661
|
+
return;
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4664
|
+
// Validated — copy to hooks dir
|
|
4307
4665
|
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
4308
|
-
|
|
4666
|
+
const hookContent = readFileSync(tmpScript, 'utf-8');
|
|
4667
|
+
writeFileSync(hookPath, hookContent);
|
|
4309
4668
|
chmodSync(hookPath, 0o755);
|
|
4310
|
-
|
|
4669
|
+
try { unlinkSync(tmpScript); } catch {}
|
|
4670
|
+
console.log(c.green + ' + ' + c.reset + 'Created ' + hookName + ' (syntax verified)');
|
|
4311
4671
|
|
|
4312
|
-
// Register in settings.json
|
|
4672
|
+
// Register in settings.json — use "Bash" matcher ONLY (never "")
|
|
4673
|
+
// "" matcher affects ALL tools and a broken hook would lock out the session
|
|
4313
4674
|
let settings = {};
|
|
4314
4675
|
if (existsSync(SETTINGS_PATH)) {
|
|
4315
4676
|
try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
|
|
@@ -4317,16 +4678,34 @@ exit 0
|
|
|
4317
4678
|
if (!settings.hooks) settings.hooks = {};
|
|
4318
4679
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
4319
4680
|
|
|
4320
|
-
//
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4681
|
+
// Register under specific matchers based on rule types (NEVER use "" matcher)
|
|
4682
|
+
const hookCmd = `bash ${hookPath}`;
|
|
4683
|
+
const hasBlocks = rules.some(r => r.type === 'block');
|
|
4684
|
+
const hasApproves = rules.some(r => r.type === 'approve');
|
|
4685
|
+
const hasProtects = rules.some(r => r.type === 'protect');
|
|
4686
|
+
|
|
4687
|
+
// Block and approve rules need Bash matcher
|
|
4688
|
+
if (hasBlocks || hasApproves) {
|
|
4689
|
+
let bashMatcher = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
|
|
4690
|
+
if (!bashMatcher) {
|
|
4691
|
+
bashMatcher = { matcher: 'Bash', hooks: [] };
|
|
4692
|
+
settings.hooks.PreToolUse.push(bashMatcher);
|
|
4693
|
+
}
|
|
4694
|
+
if (!bashMatcher.hooks.some(h => h.command === hookCmd)) {
|
|
4695
|
+
bashMatcher.hooks.push({ type: 'command', command: hookCmd });
|
|
4696
|
+
}
|
|
4325
4697
|
}
|
|
4326
4698
|
|
|
4327
|
-
|
|
4328
|
-
if (
|
|
4329
|
-
editMatcher.hooks.
|
|
4699
|
+
// Protect rules need Edit|Write matcher
|
|
4700
|
+
if (hasProtects) {
|
|
4701
|
+
let editMatcher = settings.hooks.PreToolUse.find(e => e.matcher === 'Edit|Write');
|
|
4702
|
+
if (!editMatcher) {
|
|
4703
|
+
editMatcher = { matcher: 'Edit|Write', hooks: [] };
|
|
4704
|
+
settings.hooks.PreToolUse.push(editMatcher);
|
|
4705
|
+
}
|
|
4706
|
+
if (!editMatcher.hooks.some(h => h.command === hookCmd)) {
|
|
4707
|
+
editMatcher.hooks.push({ type: 'command', command: hookCmd });
|
|
4708
|
+
}
|
|
4330
4709
|
}
|
|
4331
4710
|
|
|
4332
4711
|
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
@@ -4796,9 +5175,12 @@ async function main() {
|
|
|
4796
5175
|
if (LEARN) return learn();
|
|
4797
5176
|
if (SCAN) return scan();
|
|
4798
5177
|
if (FULL) return fullSetup();
|
|
5178
|
+
if (VALIDATE) return validateHooks();
|
|
5179
|
+
if (SAFE_MODE) return safeMode(!SAFE_MODE_OFF);
|
|
4799
5180
|
if (DOCTOR) return doctor();
|
|
4800
5181
|
if (SIMULATE_CMD) return simulate(SIMULATE_CMD);
|
|
4801
5182
|
if (PROTECT_PATH) return protect(PROTECT_PATH);
|
|
5183
|
+
if (RULES_FILE) return compileRules(RULES_FILE);
|
|
4802
5184
|
if (WATCH) return watch();
|
|
4803
5185
|
if (TEST_HOOK_IDX !== -1) return testHook(TEST_HOOK);
|
|
4804
5186
|
if (SAVE_PROFILE_IDX !== -1) return saveProfile(SAVE_PROFILE);
|
|
@@ -4913,7 +5295,7 @@ async function main() {
|
|
|
4913
5295
|
if (!exists) {
|
|
4914
5296
|
settings.hooks[trigger].push({
|
|
4915
5297
|
matcher: hook.matcher,
|
|
4916
|
-
hooks: [{ type: 'command', command: hookPath }]
|
|
5298
|
+
hooks: [{ type: 'command', command: toBashPath(hookPath) }]
|
|
4917
5299
|
});
|
|
4918
5300
|
}
|
|
4919
5301
|
}
|
|
@@ -4925,10 +5307,13 @@ async function main() {
|
|
|
4925
5307
|
console.log();
|
|
4926
5308
|
console.log(c.bold + ' Done.' + c.reset + ' ' + Object.keys(HOOKS).length + ' safety hooks installed.');
|
|
4927
5309
|
console.log(' ' + c.dim + 'Restart Claude Code to activate.' + c.reset);
|
|
4928
|
-
console.log(' ' + c.dim + 'Verify:' + c.reset + ' ' + c.blue + 'npx cc-health-check' + c.reset);
|
|
4929
5310
|
console.log();
|
|
4930
|
-
console.log(' ' + c.dim + '
|
|
4931
|
-
console.log('
|
|
5311
|
+
console.log(' ' + c.dim + 'Next steps:' + c.reset);
|
|
5312
|
+
console.log(' ' + c.blue + ' --doctor' + c.reset + ' Verify hooks work');
|
|
5313
|
+
console.log(' ' + c.blue + ' --simulate "cmd"' + c.reset + ' Test how hooks react');
|
|
5314
|
+
console.log(' ' + c.blue + ' --shield' + c.reset + ' Maximum safety (recommended)');
|
|
5315
|
+
console.log();
|
|
5316
|
+
console.log(' ' + c.dim + '22 web tools: https://yurukusa.github.io/cc-safe-setup/hub.html' + c.reset);
|
|
4932
5317
|
console.log();
|
|
4933
5318
|
}
|
|
4934
5319
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "28.4.
|
|
4
|
-
"description": "One command to make Claude Code safe.
|
|
3
|
+
"version": "28.4.2",
|
|
4
|
+
"description": "One command to make Claude Code safe. 336 hooks (8 built-in + 328 examples). 49 CLI commands. 978 tests. 5 languages.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-safe-setup": "index.mjs"
|