cc-safe-setup 29.6.40 → 29.7.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/.claude-plugin/marketplace.json +66 -0
- package/.claude-plugin/plugin.json +11 -0
- package/README.md +123 -12
- package/SETTINGS_REFERENCE.md +2 -0
- package/SKILL.md +47 -0
- package/examples/README.md +11 -1
- package/examples/auto-approve-compound-git.sh +3 -0
- package/examples/auto-compact-context-monitor.sh +35 -0
- package/examples/auto-mode-safety-enforcer.sh +57 -0
- package/examples/background-task-guard.sh +57 -0
- package/examples/broad-find-guard.sh +62 -0
- package/examples/cache-creation-spike-detector.sh +32 -0
- package/examples/case-insensitive-path-guard.sh +96 -0
- package/examples/cjk-punctuation-guard.sh +44 -0
- package/examples/clipboard-secret-guard.sh +29 -0
- package/examples/context-size-alert.sh +38 -0
- package/examples/context-usage-drift-alert.sh +33 -0
- package/examples/dangerous-pip-flag-guard.sh +51 -0
- package/examples/deny-bypass-detector.sh +143 -0
- package/examples/dotenv-read-guard.sh +48 -0
- package/examples/dotfile-protection-guard.sh +60 -0
- package/examples/effort-tracking-logger.sh +30 -0
- package/examples/financial-operation-guard.sh +47 -0
- package/examples/full-rewrite-detector.sh +63 -0
- package/examples/home-critical-bash-guard.sh +56 -0
- package/examples/idle-session-cost-alert.sh +36 -0
- package/examples/model-version-alert.sh +18 -0
- package/examples/model-version-change-alert.sh +31 -0
- package/examples/move-delete-sequence-guard.sh +92 -0
- package/examples/pii-upload-guard.sh +72 -0
- package/examples/pr-duplicate-guard.sh +14 -0
- package/examples/production-port-kill-guard.sh +60 -0
- package/examples/quota-reset-cycle-monitor.sh +30 -0
- package/examples/repo-visibility-guard.sh +33 -0
- package/examples/sandbox-relative-path-audit.sh +51 -0
- package/examples/session-agent-cost-limiter.sh +43 -0
- package/examples/session-cost-alert.sh +62 -0
- package/examples/session-memory-watchdog.sh +9 -0
- package/examples/settings-integrity-monitor.sh +55 -0
- package/examples/settings-json-model-guard.sh +89 -0
- package/examples/shell-config-truncation-guard.sh +97 -0
- package/examples/shell-wrapper-guard.sh +4 -4
- package/examples/subagent-spawn-rate-monitor.sh +34 -0
- package/examples/subcommand-chain-guard.sh +44 -0
- package/examples/system-dir-protection-guard.sh +100 -0
- package/examples/thinking-display-enforcer.sh +25 -0
- package/examples/tool-retry-budget-guard.sh +59 -0
- package/examples/worktree-branch-pollution-detector.sh +35 -0
- package/examples/worktree-create-log.sh +6 -0
- package/examples/worktree-hook-linker.sh +72 -0
- package/examples/worktree-remove-uncommitted-guard.sh +20 -0
- package/hooks/hooks.json +60 -0
- package/index.mjs +92 -6
- package/memory/market-anthropic-japan-strategy-2026-04-13.md +4 -0
- package/package.json +2 -2
- package/plugins/credential-guard/.claude-plugin/plugin.json +58 -0
- package/plugins/git-protection/.claude-plugin/plugin.json +58 -0
- package/plugins/safety-essentials/.claude-plugin/plugin.json +58 -0
- package/plugins/token-guard/.claude-plugin/plugin.json +51 -0
- package/skills/safety-setup/SKILL.md +47 -0
- package/tests/dotenv-read-guard.test.sh +65 -0
- package/tests/test-auto-mode-safety-enforcer.sh +55 -0
- package/tests/test-case-insensitive-path-guard.sh +78 -0
- package/tests/test-context-usage-drift-alert.sh +52 -0
- package/tests/test-dangerous-pip-flag-guard.sh +56 -0
- package/tests/test-dotfile-protection-guard.sh +68 -0
- package/tests/test-effort-tracking-logger.sh +55 -0
- package/tests/test-financial-operation-guard.sh +59 -0
- package/tests/test-home-critical-bash-guard.sh +59 -0
- package/tests/test-model-version-change-alert.sh +55 -0
- package/tests/test-move-delete-sequence-guard.sh +63 -0
- package/tests/test-pr-duplicate-guard.sh +29 -0
- package/tests/test-quota-reset-cycle-monitor.sh +52 -0
- package/tests/test-shell-config-truncation-guard.sh +104 -0
- package/tests/test-subagent-spawn-rate-monitor.sh +43 -0
- package/tests/test-system-dir-protection-guard.sh +81 -0
- package/tests/test-tool-retry-budget-guard.sh +75 -0
- package/tests/test-worktree-branch-pollution-detector.sh +50 -0
- package/tests/test-worktree-lifecycle-hooks.sh +29 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# worktree-branch-pollution-detector.sh — worktreeが親ブランチを汚染していないか検知
|
|
3
|
+
# Why: サブエージェントのworktree操作が親リポを予期しないブランチに移動させ、
|
|
4
|
+
# 意図しないcommit-to-mainが発生する。1週間で3回の事故報告あり (#50207)
|
|
5
|
+
# Event: PostToolUse MATCHER: Bash
|
|
6
|
+
# Action: 現在のブランチが期待値と異なる場合に警告
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
|
|
10
|
+
# 期待ブランチ(セッション開始時に記録)
|
|
11
|
+
EXPECTED_BRANCH_FILE="/tmp/cc-expected-branch-$(pwd | md5sum | cut -c1-8)"
|
|
12
|
+
|
|
13
|
+
# git管理下でなければスキップ
|
|
14
|
+
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
|
|
15
|
+
|
|
16
|
+
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null)
|
|
17
|
+
[ -z "$CURRENT_BRANCH" ] && exit 0
|
|
18
|
+
|
|
19
|
+
# 初回実行時はブランチを記録
|
|
20
|
+
if [ ! -f "$EXPECTED_BRANCH_FILE" ]; then
|
|
21
|
+
echo "$CURRENT_BRANCH" > "$EXPECTED_BRANCH_FILE"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
EXPECTED_BRANCH=$(cat "$EXPECTED_BRANCH_FILE" 2>/dev/null)
|
|
26
|
+
|
|
27
|
+
if [ "$CURRENT_BRANCH" != "$EXPECTED_BRANCH" ]; then
|
|
28
|
+
echo "⚠ BRANCH CHANGED: Expected '$EXPECTED_BRANCH' but now on '$CURRENT_BRANCH'" >&2
|
|
29
|
+
echo "This may be caused by a worktree or subagent switching your branch." >&2
|
|
30
|
+
echo "Run 'git checkout $EXPECTED_BRANCH' to return. See: #50207" >&2
|
|
31
|
+
# 新しいブランチを記録(意図的な切替かもしれない)
|
|
32
|
+
echo "$CURRENT_BRANCH" > "$EXPECTED_BRANCH_FILE"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
exit 0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
LOGFILE="${HOME}/.claude/worktree-audit.log"
|
|
2
|
+
INFO=$(cat)
|
|
3
|
+
BRANCH=$(echo "$INFO" | jq -r '.branch // "unknown"' 2>/dev/null)
|
|
4
|
+
PATH_WT=$(echo "$INFO" | jq -r '.path // "unknown"' 2>/dev/null)
|
|
5
|
+
echo "[$(date '+%Y-%m-%d %H:%M:%S')] CREATE branch=$BRANCH path=$PATH_WT" >> "$LOGFILE"
|
|
6
|
+
exit 0
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# worktree-hook-linker.sh — Auto-link settings to worktrees
|
|
3
|
+
#
|
|
4
|
+
# Solves: In git worktrees, .claude/settings.json is not found because
|
|
5
|
+
# worktrees share .git but not the working directory. All hooks
|
|
6
|
+
# become silently disabled. (#46808)
|
|
7
|
+
#
|
|
8
|
+
# How it works: On SessionStart, checks if the current directory is a
|
|
9
|
+
# git worktree. If so, creates a symlink from the worktree's
|
|
10
|
+
# .claude/settings.json to the main tree's settings. This ensures
|
|
11
|
+
# hooks work identically in worktrees.
|
|
12
|
+
#
|
|
13
|
+
# {
|
|
14
|
+
# "hooks": {
|
|
15
|
+
# "Notification": [{
|
|
16
|
+
# "matcher": "SessionStart",
|
|
17
|
+
# "hooks": [{ "type": "command", "command": "~/.claude/hooks/worktree-hook-linker.sh" }]
|
|
18
|
+
# }]
|
|
19
|
+
# }
|
|
20
|
+
# }
|
|
21
|
+
#
|
|
22
|
+
# TRIGGER: Notification
|
|
23
|
+
# MATCHER: "SessionStart"
|
|
24
|
+
|
|
25
|
+
# Detect if we're in a git worktree
|
|
26
|
+
GITDIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
|
|
27
|
+
echo "$GITDIR" | grep -q "worktrees" || exit 0
|
|
28
|
+
|
|
29
|
+
# We're in a worktree — find the main working tree
|
|
30
|
+
MAIN_GITDIR=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || exit 0
|
|
31
|
+
MAIN_WORKDIR=$(echo "$MAIN_GITDIR" | sed 's|/.git$||')
|
|
32
|
+
|
|
33
|
+
MAIN_CLAUDE_DIR="$MAIN_WORKDIR/.claude"
|
|
34
|
+
LOCAL_CLAUDE_DIR=".claude"
|
|
35
|
+
|
|
36
|
+
# Skip if main tree has no .claude directory
|
|
37
|
+
[ -d "$MAIN_CLAUDE_DIR" ] || exit 0
|
|
38
|
+
|
|
39
|
+
# Create .claude directory in worktree if needed
|
|
40
|
+
mkdir -p "$LOCAL_CLAUDE_DIR" 2>/dev/null
|
|
41
|
+
|
|
42
|
+
# Link settings files if they exist in main but not in worktree
|
|
43
|
+
for f in settings.json settings.local.json; do
|
|
44
|
+
MAIN_FILE="$MAIN_CLAUDE_DIR/$f"
|
|
45
|
+
LOCAL_FILE="$LOCAL_CLAUDE_DIR/$f"
|
|
46
|
+
|
|
47
|
+
[ ! -f "$MAIN_FILE" ] && continue
|
|
48
|
+
|
|
49
|
+
if [ ! -e "$LOCAL_FILE" ]; then
|
|
50
|
+
ln -s "$MAIN_FILE" "$LOCAL_FILE"
|
|
51
|
+
echo "Linked $f from main tree → worktree (hooks now active)" >&2
|
|
52
|
+
elif [ -L "$LOCAL_FILE" ]; then
|
|
53
|
+
# Already a symlink — verify it points to the right place
|
|
54
|
+
TARGET=$(readlink -f "$LOCAL_FILE" 2>/dev/null)
|
|
55
|
+
EXPECTED=$(readlink -f "$MAIN_FILE" 2>/dev/null)
|
|
56
|
+
if [ "$TARGET" != "$EXPECTED" ]; then
|
|
57
|
+
rm "$LOCAL_FILE"
|
|
58
|
+
ln -s "$MAIN_FILE" "$LOCAL_FILE"
|
|
59
|
+
echo "Re-linked $f (was pointing to wrong location)" >&2
|
|
60
|
+
fi
|
|
61
|
+
fi
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
# Also link hooks directory if it exists
|
|
65
|
+
MAIN_HOOKS="$MAIN_CLAUDE_DIR/hooks"
|
|
66
|
+
LOCAL_HOOKS="$LOCAL_CLAUDE_DIR/hooks"
|
|
67
|
+
if [ -d "$MAIN_HOOKS" ] && [ ! -e "$LOCAL_HOOKS" ]; then
|
|
68
|
+
ln -s "$MAIN_HOOKS" "$LOCAL_HOOKS"
|
|
69
|
+
echo "Linked hooks/ directory from main tree → worktree" >&2
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
exit 0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
INFO=$(cat)
|
|
2
|
+
PATH_WT=$(echo "$INFO" | jq -r '.path // empty' 2>/dev/null)
|
|
3
|
+
[ -z "$PATH_WT" ] && exit 0
|
|
4
|
+
[ ! -d "$PATH_WT" ] && exit 0
|
|
5
|
+
cd "$PATH_WT" 2>/dev/null || exit 0
|
|
6
|
+
DIRTY=$(git status --porcelain 2>/dev/null | wc -l)
|
|
7
|
+
if [ "$DIRTY" -gt 0 ]; then
|
|
8
|
+
echo "BLOCKED: Worktree at $PATH_WT has $DIRTY uncommitted change(s)." >&2
|
|
9
|
+
echo "Commit or stash changes before removing." >&2
|
|
10
|
+
exit 2
|
|
11
|
+
fi
|
|
12
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
13
|
+
if [ -n "$BRANCH" ]; then
|
|
14
|
+
UNPUSHED=$(git log --oneline "origin/$BRANCH..$BRANCH" 2>/dev/null | wc -l)
|
|
15
|
+
if [ "$UNPUSHED" -gt 0 ]; then
|
|
16
|
+
echo "WARNING: $UNPUSHED unpushed commit(s) on $BRANCH." >&2
|
|
17
|
+
echo "Push before removing: git push origin $BRANCH" >&2
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
exit 0
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE '^\\s*(sudo\\s+)?rm\\s+.*-[rRf]*[rR]' && ! echo \"$CMD\" | grep -qE '(node_modules|dist|build|__pycache__|/tmp)'; then echo 'BLOCKED: recursive rm on non-safe target. Use specific paths.' >&2; exit 2; fi"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Bash",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+push\\s+.*--force|git\\s+reset\\s+--hard|git\\s+clean\\s+-fd'; then echo 'BLOCKED: destructive git operation. Use safer alternatives.' >&2; exit 2; fi"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Bash",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty'); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qiE '(api.key|secret|password|token).*=.*[A-Za-z0-9]{20}'; then echo 'BLOCKED: potential credential in command. Use environment variables.' >&2; exit 2; fi"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Write|Edit",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty'); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE '\\.(env|pem|key|credentials|secret)$'; then echo 'BLOCKED: writing to sensitive file. Check if this is intentional.' >&2; exit 2; fi"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"matcher": "Bash",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "~/.claude/hooks/move-delete-sequence-guard.sh"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"matcher": "Bash",
|
|
51
|
+
"hooks": [
|
|
52
|
+
{
|
|
53
|
+
"type": "command",
|
|
54
|
+
"command": "~/.claude/hooks/system-dir-protection-guard.sh"
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
package/index.mjs
CHANGED
|
@@ -94,6 +94,7 @@ const GENERATE_CI = process.argv.includes('--generate-ci');
|
|
|
94
94
|
const REPORT = process.argv.includes('--report');
|
|
95
95
|
const QUICKFIX = process.argv.includes('--quickfix');
|
|
96
96
|
const SHIELD = process.argv.includes('--shield');
|
|
97
|
+
const OPUS47 = process.argv.includes('--opus47');
|
|
97
98
|
const ANALYZE = process.argv.includes('--analyze');
|
|
98
99
|
const TEAM = process.argv.includes('--team');
|
|
99
100
|
const MIGRATE_FROM_IDX = process.argv.findIndex(a => a === '--migrate-from');
|
|
@@ -192,8 +193,9 @@ if (HELP) {
|
|
|
192
193
|
Find hooks: npx cc-hook-registry search <keyword>
|
|
193
194
|
Test hooks: npx cc-hook-test <hook.sh>
|
|
194
195
|
|
|
195
|
-
|
|
196
|
-
Book: https://
|
|
196
|
+
Token Checkup: https://yurukusa.github.io/cc-safe-setup/token-checkup.html
|
|
197
|
+
Token Book: https://yurukusa.github.io/cc-safe-setup/token-book.html
|
|
198
|
+
Safety Guide: https://zenn.dev/yurukusa/books/6076c23b1cb18b
|
|
197
199
|
`);
|
|
198
200
|
process.exit(0);
|
|
199
201
|
}
|
|
@@ -2916,6 +2918,79 @@ async function analyze() {
|
|
|
2916
2918
|
console.log();
|
|
2917
2919
|
}
|
|
2918
2920
|
|
|
2921
|
+
async function opus47() {
|
|
2922
|
+
console.log();
|
|
2923
|
+
console.log(c.bold + ' 🚨 cc-safe-setup --opus47' + c.reset);
|
|
2924
|
+
console.log(c.dim + ' Opus 4.7 protection — fixes for known critical issues' + c.reset);
|
|
2925
|
+
console.log();
|
|
2926
|
+
|
|
2927
|
+
// First install core hooks if not already installed
|
|
2928
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
2929
|
+
let coreInstalled = 0;
|
|
2930
|
+
for (const [hookId, hookMeta] of Object.entries(HOOKS)) {
|
|
2931
|
+
const hookPath = join(HOOKS_DIR, `${hookId}.sh`);
|
|
2932
|
+
if (!existsSync(hookPath)) {
|
|
2933
|
+
writeFileSync(hookPath, SCRIPTS[hookId]);
|
|
2934
|
+
chmodSync(hookPath, 0o755);
|
|
2935
|
+
coreInstalled++;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
if (coreInstalled > 0) {
|
|
2939
|
+
// Update settings.json for core hooks
|
|
2940
|
+
let settings = {};
|
|
2941
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
2942
|
+
settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
|
|
2943
|
+
}
|
|
2944
|
+
if (!settings.hooks) settings.hooks = {};
|
|
2945
|
+
for (const [hookId, hookMeta] of Object.entries(HOOKS)) {
|
|
2946
|
+
const trigger = hookMeta.trigger;
|
|
2947
|
+
if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
|
|
2948
|
+
const hookPath = toBashPath(join(HOOKS_DIR, `${hookId}.sh`));
|
|
2949
|
+
const exists = settings.hooks[trigger].some(h => h.hooks?.some(hh => hh.command?.includes(hookId)));
|
|
2950
|
+
if (!exists) {
|
|
2951
|
+
settings.hooks[trigger].push({
|
|
2952
|
+
matcher: hookMeta.matcher,
|
|
2953
|
+
hooks: [{ type: 'command', command: hookPath }]
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
|
|
2958
|
+
console.log(c.green + ' ✓' + c.reset + ` ${coreInstalled} core safety hooks installed`);
|
|
2959
|
+
} else {
|
|
2960
|
+
console.log(c.dim + ' ✓ Core safety hooks already installed' + c.reset);
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// Install Opus 4.7-specific hooks
|
|
2964
|
+
const opus47Hooks = [
|
|
2965
|
+
{ name: 'model-version-alert', desc: 'Warns when Opus 4.7 is silently active (#49541)', issue: '4x token consumption' },
|
|
2966
|
+
{ name: 'shell-config-truncation-guard', desc: 'Blocks installer from truncating ~/.bash_profile (#49615)', issue: 'config destruction' },
|
|
2967
|
+
{ name: 'credential-exfil-guard', desc: 'Blocks credential file access (#49539, #49554)', issue: 'credential deletion' },
|
|
2968
|
+
{ name: 'home-critical-bash-guard', desc: 'Blocks destructive ops on critical dotfiles (#49554)', issue: 'home directory attacks' },
|
|
2969
|
+
];
|
|
2970
|
+
|
|
2971
|
+
console.log();
|
|
2972
|
+
console.log(c.bold + ' Opus 4.7 protection hooks:' + c.reset);
|
|
2973
|
+
let added = 0;
|
|
2974
|
+
for (const hook of opus47Hooks) {
|
|
2975
|
+
try {
|
|
2976
|
+
await installExample(hook.name);
|
|
2977
|
+
added++;
|
|
2978
|
+
} catch (e) {
|
|
2979
|
+
// installExample may exit on error, but we want to continue
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
console.log();
|
|
2984
|
+
console.log(c.bold + ' Why these hooks matter:' + c.reset);
|
|
2985
|
+
console.log(c.dim + ' Opus 4.7\'s safety classifier is hardcoded to 4.6 (#49618).' + c.reset);
|
|
2986
|
+
console.log(c.dim + ' Auto mode can\'t block dangerous commands on 4.7.' + c.reset);
|
|
2987
|
+
console.log(c.dim + ' These hooks run at process level — independent of the model.' + c.reset);
|
|
2988
|
+
console.log();
|
|
2989
|
+
console.log(c.green + ' Done.' + c.reset + ' Your setup is protected against known Opus 4.7 issues.');
|
|
2990
|
+
console.log(c.dim + ' Guide: https://yurukusa.github.io/cc-safe-setup/opus-47-survival-guide.html' + c.reset);
|
|
2991
|
+
console.log();
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2919
2994
|
async function shield() {
|
|
2920
2995
|
const { execSync } = await import('child_process');
|
|
2921
2996
|
const { readdirSync } = await import('fs');
|
|
@@ -2969,6 +3044,9 @@ async function shield() {
|
|
|
2969
3044
|
// Always include these for maximum safety
|
|
2970
3045
|
extras.push('scope-guard', 'no-sudo-guard', 'protect-claudemd', 'memory-write-guard', 'skill-gate', 'auto-approve-test', 'auto-approve-readonly');
|
|
2971
3046
|
|
|
3047
|
+
// Opus 4.7 safety: classifier is hardcoded to 4.6 (#49618) — hooks are the only defense
|
|
3048
|
+
extras.push('dotfile-protection-guard', 'home-critical-bash-guard');
|
|
3049
|
+
|
|
2972
3050
|
for (const ex of extras) {
|
|
2973
3051
|
const exPath = join(__dirname, 'examples', `${ex}.sh`);
|
|
2974
3052
|
const hookPath = join(HOOKS_DIR, `${ex}.sh`);
|
|
@@ -3118,6 +3196,9 @@ async function shield() {
|
|
|
3118
3196
|
console.log(c.dim + ' Verify: npx cc-safe-setup --verify' + c.reset);
|
|
3119
3197
|
console.log(c.dim + ' Status: npx cc-safe-setup --status' + c.reset);
|
|
3120
3198
|
console.log();
|
|
3199
|
+
console.log(c.dim + ' Burning tokens too fast? Free diagnosis:' + c.reset);
|
|
3200
|
+
console.log(c.blue + ' https://yurukusa.github.io/cc-safe-setup/token-checkup.html' + c.reset);
|
|
3201
|
+
console.log();
|
|
3121
3202
|
}
|
|
3122
3203
|
|
|
3123
3204
|
async function quickfix() {
|
|
@@ -5784,6 +5865,7 @@ async function main() {
|
|
|
5784
5865
|
if (TEAM) return team();
|
|
5785
5866
|
if (PROFILE_IDX !== -1) return profile(PROFILE);
|
|
5786
5867
|
if (ANALYZE) return analyze();
|
|
5868
|
+
if (OPUS47) return opus47();
|
|
5787
5869
|
if (SHIELD) return shield();
|
|
5788
5870
|
if (QUICKFIX) return quickfix();
|
|
5789
5871
|
if (REPORT) return report();
|
|
@@ -5898,12 +5980,16 @@ async function main() {
|
|
|
5898
5980
|
console.log(' ' + c.blue + ' --doctor' + c.reset + ' Verify hooks work');
|
|
5899
5981
|
console.log(' ' + c.blue + ' --simulate "cmd"' + c.reset + ' Test how hooks react');
|
|
5900
5982
|
console.log(' ' + c.blue + ' --shield' + c.reset + ' Maximum safety (recommended)');
|
|
5983
|
+
console.log(' ' + c.blue + ' --opus47' + c.reset + ' Opus 4.7 crisis protection');
|
|
5901
5984
|
console.log();
|
|
5902
|
-
console.log(' ' + c.dim + '
|
|
5985
|
+
console.log(' ' + c.dim + 'Free tools:' + c.reset);
|
|
5986
|
+
console.log(' ' + c.blue + ' Token Checkup' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/token-checkup.html');
|
|
5987
|
+
console.log(' ' + c.blue + ' Token Book' + c.reset + ' https://yurukusa.github.io/cc-safe-setup/token-book.html');
|
|
5988
|
+
console.log(' ' + c.dim + ' 28 web tools: https://yurukusa.github.io/cc-safe-setup/hub.html' + c.reset);
|
|
5903
5989
|
console.log();
|
|
5904
|
-
console.log(' ' + c.dim + '
|
|
5905
|
-
console.log(' ' + c.
|
|
5906
|
-
console.log(' ' + c.dim + '
|
|
5990
|
+
console.log(' ' + c.dim + 'Tokens disappearing too fast?' + c.reset);
|
|
5991
|
+
console.log(' ' + c.blue + ' Token Book' + c.reset + ' Cut consumption in half — https://yurukusa.github.io/cc-safe-setup/token-book.html');
|
|
5992
|
+
console.log(' ' + c.dim + ' Safety Guide: https://zenn.dev/yurukusa/books/6076c23b1cb18b' + c.reset);
|
|
5907
5993
|
console.log();
|
|
5908
5994
|
}
|
|
5909
5995
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "29.
|
|
4
|
-
"description": "One command to make Claude Code safe.
|
|
3
|
+
"version": "29.7.0",
|
|
4
|
+
"description": "One command to make Claude Code safe. 700 example hooks + 8 built-in. 56 CLI commands. Token consumption diagnosis. Works with Auto Mode.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cc-safe-setup": "index.mjs"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "credential-guard",
|
|
3
|
+
"description": "Protect secrets and credentials from Claude Code. Blocks writes to .env files, detects API keys in shell commands, prevents hardcoded tokens, and guards service account JSON files.",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"author": { "name": "yurukusa" },
|
|
6
|
+
"homepage": "https://yurukusa.github.io/cc-safe-setup/",
|
|
7
|
+
"repository": "https://github.com/yurukusa/cc-safe-setup",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"hooks": {
|
|
10
|
+
"PreToolUse": [
|
|
11
|
+
{
|
|
12
|
+
"matcher": "Write",
|
|
13
|
+
"hooks": [
|
|
14
|
+
{
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE '\\.env$|\\.env\\.|credentials|secret'; then echo \"BLOCKED: Writing to sensitive file: $FILE\" >&2; exit 2; fi"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Edit",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE '\\.env$|\\.env\\.|credentials|secret'; then echo \"BLOCKED: Editing sensitive file: $FILE\" >&2; exit 2; fi"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE '(sk|pk|api|key|token|secret|password)[-_]?[a-zA-Z0-9]{20,}'; then echo 'WARNING: Possible API key or token detected in command. Verify no secrets are exposed.' >&2; fi"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"matcher": "Write",
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE 'serviceaccount.*\\.json|key\\.json|credentials\\.json'; then echo \"BLOCKED: Writing to service account file: $FILE\" >&2; exit 2; fi"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"matcher": "Bash",
|
|
49
|
+
"hooks": [
|
|
50
|
+
{
|
|
51
|
+
"type": "command",
|
|
52
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'ANTHROPIC_API_KEY|OPENAI_API_KEY|AWS_SECRET|GITHUB_TOKEN|DATABASE_URL'; then echo 'WARNING: Environment variable with potential secret detected in command.' >&2; fi"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-protection",
|
|
3
|
+
"description": "Git safety hooks for Claude Code. Blocks force-push, protects main/master branch, prevents hard-reset, guards interactive rebase, and blocks git clean -fd.",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"author": { "name": "yurukusa" },
|
|
6
|
+
"homepage": "https://yurukusa.github.io/cc-safe-setup/",
|
|
7
|
+
"repository": "https://github.com/yurukusa/cc-safe-setup",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"hooks": {
|
|
10
|
+
"PreToolUse": [
|
|
11
|
+
{
|
|
12
|
+
"matcher": "Bash",
|
|
13
|
+
"hooks": [
|
|
14
|
+
{
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+push.*--force|git\\s+push.*-f\\b'; then echo 'BLOCKED: Force push. Use --force-with-lease for safer alternative.' >&2; exit 2; fi"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Bash",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+push\\s+(origin\\s+)?(main|master)\\b'; then echo 'BLOCKED: Direct push to main/master. Use a feature branch and PR.' >&2; exit 2; fi"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+reset\\s+--hard'; then echo 'BLOCKED: git reset --hard. Use git stash or git reset --soft.' >&2; exit 2; fi"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"matcher": "Bash",
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+clean\\s+-fd|git\\s+clean\\s+-f'; then echo 'BLOCKED: git clean removes untracked files permanently. Review with git clean -n first.' >&2; exit 2; fi"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"matcher": "Bash",
|
|
49
|
+
"hooks": [
|
|
50
|
+
{
|
|
51
|
+
"type": "command",
|
|
52
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+branch\\s+-D'; then echo 'BLOCKED: Force branch deletion. Use -d (safe delete) instead of -D.' >&2; exit 2; fi"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "safety-essentials",
|
|
3
|
+
"description": "5 essential safety hooks for Claude Code. Blocks rm -rf, force-push, hard-reset, .env overwrites, and package publish. The minimum viable safety net from 800+ hours of autonomous operation.",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"author": { "name": "yurukusa" },
|
|
6
|
+
"homepage": "https://yurukusa.github.io/cc-safe-setup/",
|
|
7
|
+
"repository": "https://github.com/yurukusa/cc-safe-setup",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"hooks": {
|
|
10
|
+
"PreToolUse": [
|
|
11
|
+
{
|
|
12
|
+
"matcher": "Bash",
|
|
13
|
+
"hooks": [
|
|
14
|
+
{
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'rm\\s+-r(f|F)|rm\\s+-(f|F)r|rm\\s+--force.*-r|rm\\s+-r.*--force'; then echo 'BLOCKED: rm -rf detected. Use git clean or manual deletion instead.' >&2; exit 2; fi"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Bash",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+push.*--force|git\\s+push.*-f\\b'; then echo 'BLOCKED: Force push detected. Use --force-with-lease or push normally.' >&2; exit 2; fi"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Bash",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'git\\s+reset\\s+--hard'; then echo 'BLOCKED: git reset --hard discards uncommitted changes. Use git stash instead.' >&2; exit 2; fi"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"matcher": "Write",
|
|
40
|
+
"hooks": [
|
|
41
|
+
{
|
|
42
|
+
"type": "command",
|
|
43
|
+
"command": "INPUT=$(cat); FILE=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FILE\" ] && exit 0; if echo \"$FILE\" | grep -qE '\\.env$|\\.env\\.'; then echo \"BLOCKED: Writing to environment file: $FILE\" >&2; exit 2; fi"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"matcher": "Bash",
|
|
49
|
+
"hooks": [
|
|
50
|
+
{
|
|
51
|
+
"type": "command",
|
|
52
|
+
"command": "INPUT=$(cat); CMD=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty' 2>/dev/null); [ -z \"$CMD\" ] && exit 0; if echo \"$CMD\" | grep -qE 'npm\\s+publish|yarn\\s+publish'; then echo 'BLOCKED: Package publish requires manual execution.' >&2; exit 2; fi"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "token-guard",
|
|
3
|
+
"description": "Token consumption guards for Claude Code. Warns on large file reads (100KB+), limits unique file reads per session, estimates token budget, and caps subagent spawns. From 800+ hours of autonomous operation data.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": { "name": "yurukusa" },
|
|
6
|
+
"homepage": "https://yurukusa.github.io/cc-safe-setup/token-book.html",
|
|
7
|
+
"repository": "https://github.com/yurukusa/cc-safe-setup",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"hooks": {
|
|
10
|
+
"PreToolUse": [
|
|
11
|
+
{
|
|
12
|
+
"matcher": "Read",
|
|
13
|
+
"hooks": [
|
|
14
|
+
{
|
|
15
|
+
"type": "command",
|
|
16
|
+
"command": "INPUT=$(cat); FP=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FP\" ] || [ ! -f \"$FP\" ] && exit 0; SZ=$(stat -c%s \"$FP\" 2>/dev/null || stat -f%z \"$FP\" 2>/dev/null); [ \"$SZ\" -gt 102400 ] 2>/dev/null && echo \"Warning: $(basename $FP) is $((SZ/1024))KB. Use limit parameter to read only what you need.\" || true"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Read",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": "BUDGET=${CC_READ_BUDGET:-100}; WARN=${CC_READ_WARN:-50}; T=/tmp/cc-read-budget-$PPID; INPUT=$(cat); FP=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty' 2>/dev/null); [ -z \"$FP\" ] && exit 0; C=0; if [ -f \"$T\" ]; then grep -qF \"$FP\" \"$T\" || echo \"$FP\" >> \"$T\"; C=$(wc -l < \"$T\"); else echo \"$FP\" > \"$T\"; C=1; fi; [ \"$C\" -ge \"$BUDGET\" ] && { echo \"[BLOCK] Read budget reached (${BUDGET} files). Use Glob/Grep to narrow down.\"; exit 2; }; [ \"$C\" -ge \"$WARN\" ] && echo \"Warning: ${C}/${BUDGET} files read.\""
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"matcher": "Agent",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "MAX=${CC_MAX_AGENTS:-3}; T=/tmp/cc-agents-$PPID; C=0; [ -f \"$T\" ] && C=$(cat \"$T\"); C=$((C+1)); echo $C > \"$T\"; [ $C -gt $MAX ] && { echo \"[BLOCK] Subagent limit (${MAX}). Complete existing agents first.\"; exit 2; }; [ $C -ge $MAX ] && echo \"Warning: ${C}/${MAX} subagents spawned.\""
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"PostToolUse": [
|
|
40
|
+
{
|
|
41
|
+
"matcher": {},
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "WARN=${CC_TOKEN_BUDGET:-50000}; BLOCK=${CC_TOKEN_BLOCK:-100000}; T=/tmp/cc-tokens-$PPID; INPUT=$(cat); SZ=${#INPUT}; TK=$((SZ/4)); TOTAL=0; [ -f \"$T\" ] && TOTAL=$(cat \"$T\"); TOTAL=$((TOTAL+TK)); echo $TOTAL > \"$T\"; [ $TOTAL -ge $BLOCK ] && { echo \"[BLOCK] Token budget exceeded (~${TOTAL}). Run /compact.\"; exit 2; }; [ $TOTAL -ge $WARN ] && echo \"Warning: ~${TOTAL} tokens consumed (limit: ${BLOCK}).\""
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cc-safe-setup
|
|
3
|
+
description: Safety hooks for Claude Code — 695 pre-built hooks that prevent file deletion, credential leaks, git disasters, and token waste during autonomous AI coding sessions. Install with npx cc-safe-setup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# cc-safe-setup
|
|
7
|
+
|
|
8
|
+
Safety-first configuration for Claude Code. Prevents the accidents that happen when AI writes code autonomously.
|
|
9
|
+
|
|
10
|
+
## What it does
|
|
11
|
+
|
|
12
|
+
Installs pre-built safety hooks into your Claude Code environment. These hooks run automatically before/after tool calls to block dangerous operations.
|
|
13
|
+
|
|
14
|
+
**Categories:**
|
|
15
|
+
- **File protection**: Block `rm -rf`, prevent overwriting files outside project
|
|
16
|
+
- **Git safety**: Prevent force-push to main, block `reset --hard`
|
|
17
|
+
- **Credential guards**: Stop `.env` files from being committed or read by AI
|
|
18
|
+
- **Token optimization**: Warn on large file reads, limit subagent spawning
|
|
19
|
+
- **Quality gates**: Detect lazy rewrites, verify claims before committing
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx cc-safe-setup
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This runs an interactive wizard that configures hooks based on your risk profile.
|
|
28
|
+
|
|
29
|
+
## Install individual hooks
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx cc-safe-setup --install-example large-read-guard
|
|
33
|
+
npx cc-safe-setup --install-example prevent-rm-rf
|
|
34
|
+
npx cc-safe-setup --install-example git-force-push-block
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Why hooks instead of CLAUDE.md rules
|
|
38
|
+
|
|
39
|
+
Rules in CLAUDE.md are suggestions — Claude can forget them. Hooks are enforced at the system level. A hook that blocks `rm -rf` cannot be overridden by the AI.
|
|
40
|
+
|
|
41
|
+
From 800+ hours of autonomous operation: the hooks that matter most are the ones you don't notice until something goes wrong.
|
|
42
|
+
|
|
43
|
+
## Resources
|
|
44
|
+
|
|
45
|
+
- Repository: https://github.com/yurukusa/cc-safe-setup
|
|
46
|
+
- Hook Selector (find hooks for your setup): https://yurukusa.github.io/cc-safe-setup/hook-selector.html
|
|
47
|
+
- Token Checkup (diagnose waste): https://yurukusa.github.io/cc-safe-setup/token-checkup.html
|