cc-safe-setup 28.4.1 → 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/test-before-commit.sh +17 -0
- package/examples/usage-warn.sh +13 -0
- package/index.mjs +75 -62
- 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,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',
|
|
@@ -132,52 +135,43 @@ if (HELP) {
|
|
|
132
135
|
console.log(`
|
|
133
136
|
cc-safe-setup — Make Claude Code safe for autonomous operation
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
npx cc-safe-setup Install 8 safety hooks
|
|
137
|
-
npx cc-safe-setup --
|
|
138
|
-
npx cc-safe-setup --
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
npx cc-safe-setup --guard "<rule>" Instantly enforce a rule (generate + install + activate)
|
|
173
|
-
npx cc-safe-setup --diff-hooks <path> Compare hooks between two settings files
|
|
174
|
-
npx cc-safe-setup --from-claudemd Convert CLAUDE.md rules into hooks
|
|
175
|
-
npx cc-safe-setup --health Hook health dashboard (size, permissions, age)
|
|
176
|
-
npx cc-safe-setup --migrate-from <tool> Migrate from safety-net/hooks-mastery/etc.
|
|
177
|
-
npx cc-safe-setup --team Set up project-level hooks (commit to repo for team)
|
|
178
|
-
npx cc-safe-setup --profile <level> Switch safety profile (strict/standard/minimal)
|
|
179
|
-
npx cc-safe-setup --analyze Analyze what Claude did in your last session
|
|
180
|
-
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
|
|
181
175
|
npx cc-safe-setup --quickfix Auto-detect and fix common Claude Code problems
|
|
182
176
|
npx cc-safe-setup --stats Block statistics and patterns report
|
|
183
177
|
npx cc-safe-setup --export Export hooks config for team sharing
|
|
@@ -311,6 +305,8 @@ function status() {
|
|
|
311
305
|
console.log(' ' + c.dim + '+ ' + installedExamples.length + ' example hooks' + c.reset);
|
|
312
306
|
}
|
|
313
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();
|
|
314
310
|
|
|
315
311
|
// Exit code for CI: 0 = all installed, 1 = missing hooks
|
|
316
312
|
if (missing > 0) process.exit(1);
|
|
@@ -621,7 +617,7 @@ async function installExample(name) {
|
|
|
621
617
|
|
|
622
618
|
const hookEntry = {
|
|
623
619
|
matcher: matcher,
|
|
624
|
-
hooks: [{ type: 'command', command: destPath }],
|
|
620
|
+
hooks: [{ type: 'command', command: toBashPath(destPath) }],
|
|
625
621
|
};
|
|
626
622
|
|
|
627
623
|
// Check if already installed
|
|
@@ -2220,7 +2216,7 @@ async function profile(level) {
|
|
|
2220
2216
|
|
|
2221
2217
|
// Register all hooks in settings
|
|
2222
2218
|
const hookFiles = prof.hooks.filter(h => existsSync(join(HOOKS_DIR, `${h}.sh`)));
|
|
2223
|
-
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'))}` }));
|
|
2224
2220
|
|
|
2225
2221
|
// Simplified: put all under PreToolUse Bash for now
|
|
2226
2222
|
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
@@ -3887,7 +3883,7 @@ exit 0`,
|
|
|
3887
3883
|
if (!existing.some(cmd => cmd.includes(matched.name))) {
|
|
3888
3884
|
settings.hooks[matched.trigger].push({
|
|
3889
3885
|
matcher: matched.matcher,
|
|
3890
|
-
hooks: [{ type: 'command', command: hookPath }],
|
|
3886
|
+
hooks: [{ type: 'command', command: toBashPath(hookPath) }],
|
|
3891
3887
|
});
|
|
3892
3888
|
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
3893
3889
|
console.log(c.green + ' ✓ Registered in settings.json' + c.reset);
|
|
@@ -4401,26 +4397,26 @@ async function compileRules(rulesFile) {
|
|
|
4401
4397
|
|
|
4402
4398
|
# Block dangerous commands
|
|
4403
4399
|
- block: "rm -rf on root or home"
|
|
4404
|
-
pattern: "rm
|
|
4400
|
+
pattern: "rm\\s+-rf\\s+(\\/$|~)"
|
|
4405
4401
|
|
|
4406
4402
|
- block: "git push to main"
|
|
4407
|
-
pattern: "git
|
|
4403
|
+
pattern: "git\\s+push.*(main|master)"
|
|
4408
4404
|
|
|
4409
4405
|
- block: "git force push"
|
|
4410
|
-
pattern: "git
|
|
4406
|
+
pattern: "git\\s+push.*--force"
|
|
4411
4407
|
|
|
4412
4408
|
- block: "git add .env"
|
|
4413
|
-
pattern: "git
|
|
4409
|
+
pattern: "git\\s+add.*\\.env"
|
|
4414
4410
|
|
|
4415
4411
|
# Auto-approve safe commands
|
|
4416
4412
|
- approve: "read-only commands"
|
|
4417
4413
|
commands: [cat, head, tail, ls, grep, find, which, pwd, date]
|
|
4418
4414
|
|
|
4419
4415
|
- approve: "git read commands"
|
|
4420
|
-
pattern: "
|
|
4416
|
+
pattern: "^\\s*git\\s+(status|log|diff|show|branch)"
|
|
4421
4417
|
|
|
4422
4418
|
- approve: "test runners"
|
|
4423
|
-
pattern: "
|
|
4419
|
+
pattern: "^\\s*(npm\\s+test|pytest|go\\s+test|cargo\\s+test)"
|
|
4424
4420
|
|
|
4425
4421
|
# Protect files from edits
|
|
4426
4422
|
- protect: ".env"
|
|
@@ -4444,8 +4440,8 @@ async function compileRules(rulesFile) {
|
|
|
4444
4440
|
const blockMatch = trimmed.match(/^-\s+block:\s+"(.+)"/);
|
|
4445
4441
|
const approveMatch = trimmed.match(/^-\s+approve:\s+"(.+)"/);
|
|
4446
4442
|
const protectMatch = trimmed.match(/^-\s+protect:\s+"(.+)"/);
|
|
4447
|
-
const patternMatch = trimmed.match(
|
|
4448
|
-
const commandsMatch = trimmed.match(
|
|
4443
|
+
const patternMatch = trimmed.match(/^pattern:\s+"(.+)"/);
|
|
4444
|
+
const commandsMatch = trimmed.match(/^commands:\s+\[(.+)\]/);
|
|
4449
4445
|
|
|
4450
4446
|
if (blockMatch) { current = { type: 'block', name: blockMatch[1] }; rules.push(current); }
|
|
4451
4447
|
else if (approveMatch) { current = { type: 'approve', name: approveMatch[1] }; rules.push(current); }
|
|
@@ -4539,6 +4535,20 @@ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
|
4539
4535
|
writeFileSync(hookPath, script);
|
|
4540
4536
|
chmodSync(hookPath, 0o755);
|
|
4541
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
|
+
|
|
4542
4552
|
// Register in settings.json
|
|
4543
4553
|
let settings = {};
|
|
4544
4554
|
if (existsSync(SETTINGS_PATH)) {
|
|
@@ -5285,7 +5295,7 @@ async function main() {
|
|
|
5285
5295
|
if (!exists) {
|
|
5286
5296
|
settings.hooks[trigger].push({
|
|
5287
5297
|
matcher: hook.matcher,
|
|
5288
|
-
hooks: [{ type: 'command', command: hookPath }]
|
|
5298
|
+
hooks: [{ type: 'command', command: toBashPath(hookPath) }]
|
|
5289
5299
|
});
|
|
5290
5300
|
}
|
|
5291
5301
|
}
|
|
@@ -5297,10 +5307,13 @@ async function main() {
|
|
|
5297
5307
|
console.log();
|
|
5298
5308
|
console.log(c.bold + ' Done.' + c.reset + ' ' + Object.keys(HOOKS).length + ' safety hooks installed.');
|
|
5299
5309
|
console.log(' ' + c.dim + 'Restart Claude Code to activate.' + c.reset);
|
|
5300
|
-
console.log(' ' + c.dim + 'Verify:' + c.reset + ' ' + c.blue + 'npx cc-health-check' + c.reset);
|
|
5301
5310
|
console.log();
|
|
5302
|
-
console.log(' ' + c.dim + '
|
|
5303
|
-
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);
|
|
5304
5317
|
console.log();
|
|
5305
5318
|
}
|
|
5306
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"
|