cc-safe-setup 28.3.5 → 28.4.0

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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  **One command to make Claude Code safe for autonomous operation.** [日本語](docs/README.ja.md)
8
8
 
9
- 8 built-in + 313 examples = **321 hooks**. 45 CLI commands. 941 tests. 5 languages. [**Hub**](https://yurukusa.github.io/cc-safe-setup/hub.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)
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
10
 
11
11
  ```bash
12
12
  npx cc-safe-setup
@@ -110,6 +110,8 @@ Each hook exists because a real incident happened without it.
110
110
  | `--init-project` | Full project setup (hooks + CLAUDE.md + CI) |
111
111
  | `--score` | CI-friendly safety score (exit 1 if below threshold) |
112
112
  | `--test-hook <name>` | Test a specific hook with sample input |
113
+ | `--simulate "cmd"` | Preview how all hooks react to a command |
114
+ | `--protect <path>` | Block edits to a file or directory |
113
115
  | `--changelog` | Show what changed in each version |
114
116
  | `--report` | Generate safety report |
115
117
  | `--help` | Show help |
@@ -127,6 +129,11 @@ Each hook exists because a real incident happened without it.
127
129
  | Choose a safety level | `npx cc-safe-setup --profile strict` |
128
130
  | See what Claude blocked today | `npx cc-safe-setup --replay` |
129
131
  | Know why a hook exists | `npx cc-safe-setup --why destructive-guard` |
132
+ | Block silent memory file edits | `npx cc-safe-setup --install-example memory-write-guard` |
133
+ | Stop built-in skills editing opaquely | `npx cc-safe-setup --install-example skill-gate` |
134
+ | Diagnose why hooks aren't working | `npx cc-safe-setup --doctor` |
135
+ | Preview how hooks react to a command | `npx cc-safe-setup --simulate "git push origin main"` |
136
+ | Protect a specific file from edits | `npx cc-safe-setup --protect .env` |
130
137
  | Migrate from Cursor/Windsurf | [Migration Guide](https://yurukusa.github.io/cc-safe-setup/migration-guide.html) |
131
138
 
132
139
  ## How It Works
@@ -0,0 +1,31 @@
1
+ INPUT=$(cat)
2
+ STATE_DIR="${HOME}/.claude"
3
+ COUNTER_FILE="${STATE_DIR}/session-call-count"
4
+ PREP_FLAG="${STATE_DIR}/compact-prep-done"
5
+ CHECKPOINT=".claude/pre-compact-checkpoint.md"
6
+ COUNT=0
7
+ [ -f "$COUNTER_FILE" ] && COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
8
+ if [ "$COUNT" -eq 0 ]; then
9
+ COUNT=1
10
+ echo "$COUNT" > "$COUNTER_FILE"
11
+ else
12
+ COUNT=$((COUNT + 1))
13
+ echo "$COUNT" > "$COUNTER_FILE"
14
+ fi
15
+ THRESHOLD=${CC_COMPACT_PREP_THRESHOLD:-200}
16
+ if (( COUNT >= THRESHOLD )) && [ ! -f "$PREP_FLAG" ]; then
17
+ mkdir -p "$(dirname "$CHECKPOINT")" 2>/dev/null
18
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "?")
19
+ DIRTY=$(git status --porcelain 2>/dev/null | wc -l)
20
+ LAST_5=$(git log --oneline -5 2>/dev/null)
21
+ cat > "$CHECKPOINT" << CKPT
22
+ Saved: $(date -Iseconds) | Tool call: #${COUNT}
23
+ Branch: ${BRANCH} | Dirty files: ${DIRTY}
24
+ ${LAST_5}
25
+ Read this file to understand what you were working on before context was compacted.
26
+ Check git status and git log for current state. Continue from the last commit.
27
+ CKPT
28
+ touch "$PREP_FLAG"
29
+ echo "NOTICE: Context may compact soon (call #${COUNT}). Checkpoint saved to ${CHECKPOINT}" >&2
30
+ fi
31
+ exit 0
@@ -0,0 +1,15 @@
1
+ INPUT=$(cat)
2
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
3
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
4
+ [[ "$TOOL" != "Edit" && "$TOOL" != "Write" ]] && exit 0
5
+ [ -z "$FILE" ] && exit 0
6
+ git rev-parse --git-dir >/dev/null 2>&1 || exit 0
7
+ [ ! -f "$FILE" ] && exit 0
8
+ CKPT_DIR=".claude/checkpoints"
9
+ mkdir -p "$CKPT_DIR" 2>/dev/null
10
+ BASENAME=$(basename "$FILE")
11
+ TIMESTAMP=$(date +%H%M%S)
12
+ CKPT_FILE="${CKPT_DIR}/${BASENAME}.${TIMESTAMP}.bak"
13
+ cp "$FILE" "$CKPT_FILE" 2>/dev/null
14
+ ls -t "${CKPT_DIR}/${BASENAME}".*.bak 2>/dev/null | tail -n +21 | xargs rm -f 2>/dev/null
15
+ exit 0
@@ -0,0 +1,30 @@
1
+ INPUT=$(cat)
2
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
3
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
4
+ [[ "$TOOL" != "Edit" && "$TOOL" != "Write" ]] && exit 0
5
+ [ -z "$FILE" ] && exit 0
6
+ [ ! -f "$FILE" ] && { echo "WARNING: File does not exist after edit: $FILE" >&2; exit 0; }
7
+ SIZE=$(wc -c < "$FILE" 2>/dev/null || echo 0)
8
+ if [ "$SIZE" -eq 0 ]; then
9
+ echo "WARNING: File is empty after edit: $FILE (possible truncation)" >&2
10
+ exit 0
11
+ fi
12
+ if [ "$TOOL" = "Edit" ]; then
13
+ NEW_STR=$(echo "$INPUT" | jq -r '.tool_input.new_string // empty' 2>/dev/null)
14
+ if [ -n "$NEW_STR" ]; then
15
+ FIRST_LINE=$(echo "$NEW_STR" | head -1)
16
+ if [ -n "$FIRST_LINE" ] && ! grep -qF "$FIRST_LINE" "$FILE" 2>/dev/null; then
17
+ echo "WARNING: Edit may not have applied — new_string not found in $FILE" >&2
18
+ fi
19
+ fi
20
+ fi
21
+ if grep -qE '^(<<<<<<<|=======|>>>>>>>)' "$FILE" 2>/dev/null; then
22
+ echo "WARNING: Merge conflict markers detected in $FILE after edit" >&2
23
+ fi
24
+ if [ "$SIZE" -lt 10 ]; then
25
+ case "$FILE" in
26
+ *.json|*.yaml|*.yml|*.toml) ;; # Config files can be small
27
+ *) echo "WARNING: File suspiciously small ($SIZE bytes) after edit: $FILE" >&2 ;;
28
+ esac
29
+ fi
30
+ exit 0
@@ -0,0 +1,30 @@
1
+ INPUT=$(cat)
2
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
3
+ STATE_DIR="${HOME}/.claude"
4
+ COUNTER_FILE="${STATE_DIR}/session-call-count"
5
+ STATE_FILE=".claude/session-state.md"
6
+ SAVE_INTERVAL=${CC_STATE_SAVE_INTERVAL:-50}
7
+ mkdir -p "${STATE_DIR}" 2>/dev/null
8
+ mkdir -p "$(dirname "${STATE_FILE}")" 2>/dev/null
9
+ COUNT=0
10
+ [ -f "$COUNTER_FILE" ] && COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
11
+ COUNT=$((COUNT + 1))
12
+ echo "$COUNT" > "$COUNTER_FILE"
13
+ if (( COUNT % SAVE_INTERVAL == 0 )); then
14
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
15
+ MODIFIED=$(git diff --name-only 2>/dev/null | head -10)
16
+ STAGED=$(git diff --cached --name-only 2>/dev/null | head -10)
17
+ RECENT_COMMITS=$(git log --oneline -5 2>/dev/null)
18
+ cat > "$STATE_FILE" << STATE
19
+ Updated: $(date -Iseconds)
20
+ ${BRANCH}
21
+ ${MODIFIED:-none}
22
+ ${STAGED:-none}
23
+ ${RECENT_COMMITS:-none}
24
+ ${COUNT}
25
+ ---
26
+ *Read this file after compaction to restore context.*
27
+ STATE
28
+ echo "Session state saved (call #${COUNT})" >&2
29
+ fi
30
+ exit 0
@@ -0,0 +1,34 @@
1
+ INPUT=$(cat)
2
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
3
+ [[ "$TOOL" != "Agent" ]] && exit 0
4
+ MAX_AGENTS=${CC_MAX_SUBAGENTS:-5}
5
+ TRACKER="${HOME}/.claude/active-agents"
6
+ mkdir -p "$(dirname "$TRACKER")" 2>/dev/null
7
+ NOW=$(date +%s)
8
+ ACTIVE=0
9
+ if [ -f "$TRACKER" ]; then
10
+ while IFS= read -r line; do
11
+ TS=$(echo "$line" | cut -d'|' -f1)
12
+ AGE=$(( NOW - TS ))
13
+ if (( AGE < 1800 )); then
14
+ ACTIVE=$((ACTIVE + 1))
15
+ fi
16
+ done < "$TRACKER"
17
+ fi
18
+ if (( ACTIVE >= MAX_AGENTS )); then
19
+ echo "BLOCKED: $ACTIVE active subagents (max: $MAX_AGENTS)." >&2
20
+ echo "Wait for existing agents to complete before spawning more." >&2
21
+ echo "Override: CC_MAX_SUBAGENTS=10" >&2
22
+ exit 2
23
+ fi
24
+ echo "${NOW}|agent" >> "$TRACKER"
25
+ if [ -f "$TRACKER" ]; then
26
+ TMP=$(mktemp)
27
+ while IFS= read -r line; do
28
+ TS=$(echo "$line" | cut -d'|' -f1)
29
+ AGE=$(( NOW - TS ))
30
+ (( AGE < 1800 )) && echo "$line"
31
+ done < "$TRACKER" > "$TMP"
32
+ mv "$TMP" "$TRACKER"
33
+ fi
34
+ exit 0
package/index.mjs CHANGED
@@ -114,6 +114,10 @@ const SUGGEST = process.argv.includes('--suggest');
114
114
  const INIT_PROJECT = process.argv.includes('--init-project');
115
115
  const SCORE_ONLY = process.argv.includes('--score');
116
116
  const CHANGELOG_CMD = process.argv.includes('--changelog');
117
+ const SIMULATE_IDX = process.argv.findIndex(a => a === '--simulate');
118
+ const SIMULATE_CMD = SIMULATE_IDX !== -1 ? process.argv.slice(SIMULATE_IDX + 1).join(' ') : null;
119
+ const PROTECT_IDX = process.argv.findIndex(a => a === '--protect');
120
+ const PROTECT_PATH = PROTECT_IDX !== -1 ? process.argv[PROTECT_IDX + 1] : null;
117
121
  const TEST_HOOK_IDX = process.argv.findIndex(a => a === '--test-hook');
118
122
  const TEST_HOOK = TEST_HOOK_IDX !== -1 ? process.argv[TEST_HOOK_IDX + 1] : null;
119
123
  const WHY_IDX = process.argv.findIndex(a => a === '--why');
@@ -147,6 +151,8 @@ if (HELP) {
147
151
  npx cc-safe-setup --diff <file> Compare your settings with another file
148
152
  npx cc-safe-setup --lint Static analysis of hook configuration
149
153
  npx cc-safe-setup --doctor Diagnose why hooks aren't working
154
+ npx cc-safe-setup --simulate "rm -rf /" See how hooks react to a command
155
+ npx cc-safe-setup --protect .env Block edits to a specific file/dir
150
156
  npx cc-safe-setup --watch Live dashboard of blocked commands
151
157
  npx cc-safe-setup --create "<desc>" Generate a custom hook from description
152
158
  npx cc-safe-setup --test-hook <name> Test a specific hook with sample inputs
@@ -4260,6 +4266,218 @@ async function watch() {
4260
4266
  }
4261
4267
  }
4262
4268
 
4269
+ async function protect(targetPath) {
4270
+ const { resolve, basename } = await import('path');
4271
+
4272
+ console.log();
4273
+ console.log(c.bold + ' cc-safe-setup --protect' + c.reset);
4274
+
4275
+ // Normalize path
4276
+ const normalized = targetPath.replace(/\\/g, '/');
4277
+ const safeName = basename(normalized).replace(/[^a-zA-Z0-9._-]/g, '_');
4278
+ const hookName = `protect-${safeName}.sh`;
4279
+ const hookPath = join(HOOKS_DIR, hookName);
4280
+
4281
+ // Generate hook script
4282
+ const isDir = normalized.endsWith('/');
4283
+ const pattern = isDir ? `*${normalized}*` : normalized;
4284
+
4285
+ const script = `#!/bin/bash
4286
+ # Auto-generated by: npx cc-safe-setup --protect ${targetPath}
4287
+ # Blocks Edit/Write to: ${normalized}
4288
+ INPUT=$(cat)
4289
+ TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
4290
+ FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
4291
+
4292
+ [[ "$TOOL" != "Edit" && "$TOOL" != "Write" ]] && exit 0
4293
+ [ -z "$FILE" ] && exit 0
4294
+
4295
+ ${isDir
4296
+ ? `if [[ "$FILE" == *"${normalized}"* ]]; then`
4297
+ : `if [[ "$FILE" == *"${normalized}" ]] || [[ "$FILE" == *"/${normalized}" ]]; then`
4298
+ }
4299
+ jq -n '{"decision":"block","reason":"Protected path: ${normalized}"}'
4300
+ exit 0
4301
+ fi
4302
+
4303
+ exit 0
4304
+ `;
4305
+
4306
+ // Write hook
4307
+ mkdirSync(HOOKS_DIR, { recursive: true });
4308
+ writeFileSync(hookPath, script);
4309
+ chmodSync(hookPath, 0o755);
4310
+ console.log(c.green + ' + ' + c.reset + 'Created ' + hookName);
4311
+
4312
+ // Register in settings.json
4313
+ let settings = {};
4314
+ if (existsSync(SETTINGS_PATH)) {
4315
+ try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
4316
+ }
4317
+ if (!settings.hooks) settings.hooks = {};
4318
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
4319
+
4320
+ // Find or create Edit|Write matcher
4321
+ let editMatcher = settings.hooks.PreToolUse.find(e => e.matcher === 'Edit|Write');
4322
+ if (!editMatcher) {
4323
+ editMatcher = { matcher: 'Edit|Write', hooks: [] };
4324
+ settings.hooks.PreToolUse.push(editMatcher);
4325
+ }
4326
+
4327
+ const hookCmd = `bash ${hookPath}`;
4328
+ if (!editMatcher.hooks.some(h => h.command === hookCmd)) {
4329
+ editMatcher.hooks.push({ type: 'command', command: hookCmd });
4330
+ }
4331
+
4332
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
4333
+ console.log(c.green + ' + ' + c.reset + 'Registered in settings.json');
4334
+
4335
+ console.log();
4336
+ console.log(c.bold + ' Protected: ' + c.reset + c.blue + normalized + c.reset);
4337
+ console.log(c.dim + ' Edit and Write to this path will be blocked.' + c.reset);
4338
+ console.log(c.dim + ' Restart Claude Code to activate.' + c.reset);
4339
+ console.log();
4340
+
4341
+ // Show how to unprotect
4342
+ console.log(c.dim + ' To unprotect: rm ' + hookPath + c.reset);
4343
+ console.log(c.dim + ' Then remove the entry from ~/.claude/settings.json' + c.reset);
4344
+ console.log();
4345
+ }
4346
+
4347
+ async function simulate(command) {
4348
+ const { spawnSync } = await import('child_process');
4349
+ const { readdirSync } = await import('fs');
4350
+
4351
+ console.log();
4352
+ console.log(c.bold + ' cc-safe-setup --simulate' + c.reset);
4353
+ console.log(c.dim + ' Simulating how hooks would react to:' + c.reset);
4354
+ console.log(c.blue + ' $ ' + command + c.reset);
4355
+ console.log();
4356
+
4357
+ // Build fake PreToolUse JSON for Bash
4358
+ const fakeInput = JSON.stringify({
4359
+ tool_name: 'Bash',
4360
+ tool_input: { command }
4361
+ });
4362
+
4363
+ // Read settings to find registered hooks
4364
+ let settings = {};
4365
+ if (existsSync(SETTINGS_PATH)) {
4366
+ try { settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8')); } catch {}
4367
+ }
4368
+
4369
+ const hookEntries = settings.hooks?.PreToolUse || [];
4370
+ let tested = 0;
4371
+ let approved = 0;
4372
+ let blocked = 0;
4373
+ let passthrough = 0;
4374
+
4375
+ for (const entry of hookEntries) {
4376
+ const matcher = entry.matcher || '';
4377
+ // Check if matcher includes Bash (or matches all)
4378
+ if (matcher && !matcher.match(/Bash/i) && matcher !== '') continue;
4379
+
4380
+ for (const h of (entry.hooks || [])) {
4381
+ if (h.type !== 'command') continue;
4382
+ const cmd = h.command;
4383
+
4384
+ // Extract script name for display
4385
+ const parts = cmd.split(/\s+/);
4386
+ const scriptPath = parts[parts.length - 1];
4387
+ const scriptName = scriptPath.split('/').pop().replace('.sh', '');
4388
+
4389
+ // Resolve path
4390
+ const resolved = scriptPath.replace(/^~/, HOME);
4391
+ if (!existsSync(resolved)) {
4392
+ console.log(c.yellow + ' ? ' + c.reset + scriptName + c.dim + ' (script not found)' + c.reset);
4393
+ continue;
4394
+ }
4395
+
4396
+ tested++;
4397
+
4398
+ // Run the hook with fake input
4399
+ const result = spawnSync('bash', [resolved], {
4400
+ input: fakeInput,
4401
+ timeout: 5000,
4402
+ stdio: ['pipe', 'pipe', 'pipe'],
4403
+ });
4404
+
4405
+ const stdout = (result.stdout || '').toString().trim();
4406
+ const stderr = (result.stderr || '').toString().trim();
4407
+ const exitCode = result.status;
4408
+
4409
+ if (exitCode === 2) {
4410
+ blocked++;
4411
+ console.log(c.red + ' ✗ BLOCK ' + c.reset + c.bold + scriptName + c.reset);
4412
+ if (stderr) console.log(c.dim + ' ' + stderr.split('\n')[0] + c.reset);
4413
+ } else if (stdout.includes('"approve"') || stdout.includes('"decision":"approve"')) {
4414
+ approved++;
4415
+ let reason = '';
4416
+ try { reason = JSON.parse(stdout).reason || JSON.parse(stdout).decision; } catch {}
4417
+ console.log(c.green + ' ✓ APPROVE ' + c.reset + scriptName + (reason ? c.dim + ' (' + reason + ')' + c.reset : ''));
4418
+ } else {
4419
+ passthrough++;
4420
+ console.log(c.dim + ' · pass ' + scriptName + c.reset);
4421
+ }
4422
+ }
4423
+ }
4424
+
4425
+ // Also check installed hook scripts not in settings
4426
+ if (existsSync(HOOKS_DIR)) {
4427
+ const hookFiles = readdirSync(HOOKS_DIR).filter(f => f.endsWith('.sh'));
4428
+ const registeredScripts = new Set();
4429
+ for (const entry of hookEntries) {
4430
+ for (const h of (entry.hooks || [])) {
4431
+ if (h.command) registeredScripts.add(h.command.split('/').pop().replace('.sh', ''));
4432
+ }
4433
+ }
4434
+
4435
+ const unregistered = hookFiles.filter(f => !registeredScripts.has(f.replace('.sh', '')));
4436
+ if (unregistered.length > 0) {
4437
+ console.log();
4438
+ console.log(c.dim + ' Unregistered hooks (not in settings.json):' + c.reset);
4439
+ for (const f of unregistered.slice(0, 5)) {
4440
+ const resolved = join(HOOKS_DIR, f);
4441
+ const result = spawnSync('bash', [resolved], {
4442
+ input: fakeInput,
4443
+ timeout: 5000,
4444
+ stdio: ['pipe', 'pipe', 'pipe'],
4445
+ });
4446
+ const exitCode = result.status;
4447
+ const stdout = (result.stdout || '').toString().trim();
4448
+ const name = f.replace('.sh', '');
4449
+
4450
+ if (exitCode === 2) {
4451
+ console.log(c.red + ' ✗ BLOCK ' + c.reset + name + c.dim + ' (unregistered)' + c.reset);
4452
+ } else if (stdout.includes('"approve"')) {
4453
+ console.log(c.green + ' ✓ APPROVE ' + c.reset + name + c.dim + ' (unregistered)' + c.reset);
4454
+ } else {
4455
+ console.log(c.dim + ' · pass ' + name + ' (unregistered)' + c.reset);
4456
+ }
4457
+ }
4458
+ if (unregistered.length > 5) {
4459
+ console.log(c.dim + ' ... and ' + (unregistered.length - 5) + ' more' + c.reset);
4460
+ }
4461
+ }
4462
+ }
4463
+
4464
+ // Summary
4465
+ console.log();
4466
+ console.log(c.bold + ' Summary: ' + c.reset +
4467
+ (blocked > 0 ? c.red + blocked + ' blocked' + c.reset + ' · ' : '') +
4468
+ (approved > 0 ? c.green + approved + ' approved' + c.reset + ' · ' : '') +
4469
+ passthrough + ' passthrough');
4470
+
4471
+ if (blocked > 0) {
4472
+ console.log(c.red + ' → This command would be BLOCKED' + c.reset);
4473
+ } else if (approved > 0) {
4474
+ console.log(c.green + ' → This command would be auto-approved (no prompt)' + c.reset);
4475
+ } else {
4476
+ console.log(c.dim + ' → This command would trigger a permission prompt' + c.reset);
4477
+ }
4478
+ console.log();
4479
+ }
4480
+
4263
4481
  async function doctor() {
4264
4482
  const { execSync, spawnSync } = await import('child_process');
4265
4483
  const { statSync, readdirSync } = await import('fs');
@@ -4579,6 +4797,8 @@ async function main() {
4579
4797
  if (SCAN) return scan();
4580
4798
  if (FULL) return fullSetup();
4581
4799
  if (DOCTOR) return doctor();
4800
+ if (SIMULATE_CMD) return simulate(SIMULATE_CMD);
4801
+ if (PROTECT_PATH) return protect(PROTECT_PATH);
4582
4802
  if (WATCH) return watch();
4583
4803
  if (TEST_HOOK_IDX !== -1) return testHook(TEST_HOOK);
4584
4804
  if (SAVE_PROFILE_IDX !== -1) return saveProfile(SAVE_PROFILE);
@@ -4707,7 +4927,7 @@ async function main() {
4707
4927
  console.log(' ' + c.dim + 'Restart Claude Code to activate.' + c.reset);
4708
4928
  console.log(' ' + c.dim + 'Verify:' + c.reset + ' ' + c.blue + 'npx cc-health-check' + c.reset);
4709
4929
  console.log();
4710
- console.log(' ' + c.dim + 'Full kit (16 hooks + templates + tools):' + c.reset);
4930
+ console.log(' ' + c.dim + 'Need more? 16 hooks + templates for autonomous teams:' + c.reset);
4711
4931
  console.log(' https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=npm&utm_medium=cli&utm_campaign=safe-setup');
4712
4932
  console.log();
4713
4933
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "28.3.5",
3
+ "version": "28.4.0",
4
4
  "description": "One command to make Claude Code safe. 327 hooks (8 built-in + 319 examples). 45 CLI commands. 941 tests. 5 languages.",
5
5
  "main": "index.mjs",
6
6
  "bin": {