cc-safe-setup 29.6.39 → 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 +133 -12
- package/SETTINGS_REFERENCE.md +2 -0
- package/SKILL.md +47 -0
- package/TROUBLESHOOTING.md +26 -0
- package/examples/README.md +11 -1
- package/examples/activity-logger.sh +58 -0
- package/examples/allow-claude-settings.sh +3 -2
- package/examples/allow-git-hooks-dir.sh +3 -2
- package/examples/allow-protected-dirs.sh +3 -2
- 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/bash-heuristic-approver.sh +1 -1
- 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/decision-warn.sh +59 -0
- package/examples/deny-bypass-detector.sh +143 -0
- package/examples/direnv-auto-reload.sh +9 -2
- package/examples/dotenv-commit-guard.sh +11 -5
- 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/proof-log-session.sh +62 -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 +108 -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
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
|
}
|
|
@@ -690,6 +692,14 @@ function examples() {
|
|
|
690
692
|
'write-overwrite-confirm.sh': 'Warn when Write tool overwrites large files',
|
|
691
693
|
'write-secret-guard.sh': 'Block secrets from being written to files',
|
|
692
694
|
'write-shrink-guard.sh': 'Block writes that drastically shrink files',
|
|
695
|
+
'cch-cache-guard.sh': 'Block reads of session files to prevent cache poisoning',
|
|
696
|
+
'conversation-history-guard.sh': 'Block modifications to conversation history',
|
|
697
|
+
'image-file-validator.sh': 'Block reads of fake image files that corrupt sessions',
|
|
698
|
+
'permission-denial-enforcer.sh': 'Block alternative write methods after permission denial',
|
|
699
|
+
'read-only-mode.sh': 'Block all file modifications and destructive commands',
|
|
700
|
+
'replace-all-guard.sh': 'Warn when Edit uses replace_all: true',
|
|
701
|
+
'task-integrity-guard.sh': 'Prevent Claude from deleting tasks to hide unfinished work',
|
|
702
|
+
'working-directory-fence.sh': 'Block file operations outside current working directory',
|
|
693
703
|
},
|
|
694
704
|
'Auto-Approve': {
|
|
695
705
|
'allow-claude-settings.sh': 'PermissionRequest hook',
|
|
@@ -927,6 +937,9 @@ function examples() {
|
|
|
927
937
|
'tool-call-rate-limiter.sh': 'Prevent runaway tool calls',
|
|
928
938
|
'tool-file-logger.sh': 'Log file paths from Read/Write/Edit to stderr',
|
|
929
939
|
'usage-cache-local.sh': 'Cache usage info locally to avoid API calls',
|
|
940
|
+
'compact-alert-notification.sh': 'Warn when context compaction is imminent',
|
|
941
|
+
'prompt-usage-logger.sh': 'Log every prompt with timestamps',
|
|
942
|
+
'subagent-error-detector.sh': 'Detect failed subagent results',
|
|
930
943
|
},
|
|
931
944
|
'Recovery': {
|
|
932
945
|
'auto-checkpoint.sh': 'Auto-commit after every edit for rollback protection',
|
|
@@ -956,6 +969,10 @@ function examples() {
|
|
|
956
969
|
'session-summary.sh': 'Session Summary',
|
|
957
970
|
'settings-auto-backup.sh': 'Auto-backup settings on session start',
|
|
958
971
|
'terminal-state-restore.sh': 'terminal-state-restore — restore terminal to clean state on session exit',
|
|
972
|
+
'pre-compact-transcript-backup.sh': 'Backup transcript before compaction',
|
|
973
|
+
'ripgrep-permission-fix.sh': 'Auto-fix ripgrep execute permission on session start',
|
|
974
|
+
'session-backup-on-start.sh': 'Backup session JSONL files on start',
|
|
975
|
+
'session-index-repair.sh': 'Rebuild sessions-index.json on exit',
|
|
959
976
|
},
|
|
960
977
|
'UX': {
|
|
961
978
|
'auto-answer-question.sh': 'Auto-answer AskUserQuestion for headless/autonomous mode',
|
|
@@ -1028,6 +1045,7 @@ function examples() {
|
|
|
1028
1045
|
},
|
|
1029
1046
|
'Other': {
|
|
1030
1047
|
'token-spike-alert.sh': 'Alert on abnormal token consumption per turn',
|
|
1048
|
+
'mcp-warmup-wait.sh': 'Wait for MCP servers to be ready on session start',
|
|
1031
1049
|
},
|
|
1032
1050
|
};
|
|
1033
1051
|
|
|
@@ -2900,6 +2918,79 @@ async function analyze() {
|
|
|
2900
2918
|
console.log();
|
|
2901
2919
|
}
|
|
2902
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
|
+
|
|
2903
2994
|
async function shield() {
|
|
2904
2995
|
const { execSync } = await import('child_process');
|
|
2905
2996
|
const { readdirSync } = await import('fs');
|
|
@@ -2953,6 +3044,9 @@ async function shield() {
|
|
|
2953
3044
|
// Always include these for maximum safety
|
|
2954
3045
|
extras.push('scope-guard', 'no-sudo-guard', 'protect-claudemd', 'memory-write-guard', 'skill-gate', 'auto-approve-test', 'auto-approve-readonly');
|
|
2955
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
|
+
|
|
2956
3050
|
for (const ex of extras) {
|
|
2957
3051
|
const exPath = join(__dirname, 'examples', `${ex}.sh`);
|
|
2958
3052
|
const hookPath = join(HOOKS_DIR, `${ex}.sh`);
|
|
@@ -3102,6 +3196,9 @@ async function shield() {
|
|
|
3102
3196
|
console.log(c.dim + ' Verify: npx cc-safe-setup --verify' + c.reset);
|
|
3103
3197
|
console.log(c.dim + ' Status: npx cc-safe-setup --status' + c.reset);
|
|
3104
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();
|
|
3105
3202
|
}
|
|
3106
3203
|
|
|
3107
3204
|
async function quickfix() {
|
|
@@ -5768,6 +5865,7 @@ async function main() {
|
|
|
5768
5865
|
if (TEAM) return team();
|
|
5769
5866
|
if (PROFILE_IDX !== -1) return profile(PROFILE);
|
|
5770
5867
|
if (ANALYZE) return analyze();
|
|
5868
|
+
if (OPUS47) return opus47();
|
|
5771
5869
|
if (SHIELD) return shield();
|
|
5772
5870
|
if (QUICKFIX) return quickfix();
|
|
5773
5871
|
if (REPORT) return report();
|
|
@@ -5882,12 +5980,16 @@ async function main() {
|
|
|
5882
5980
|
console.log(' ' + c.blue + ' --doctor' + c.reset + ' Verify hooks work');
|
|
5883
5981
|
console.log(' ' + c.blue + ' --simulate "cmd"' + c.reset + ' Test how hooks react');
|
|
5884
5982
|
console.log(' ' + c.blue + ' --shield' + c.reset + ' Maximum safety (recommended)');
|
|
5983
|
+
console.log(' ' + c.blue + ' --opus47' + c.reset + ' Opus 4.7 crisis protection');
|
|
5885
5984
|
console.log();
|
|
5886
|
-
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);
|
|
5887
5989
|
console.log();
|
|
5888
|
-
console.log(' ' + c.dim + '
|
|
5889
|
-
console.log(' ' + c.
|
|
5890
|
-
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);
|
|
5891
5993
|
console.log();
|
|
5892
5994
|
}
|
|
5893
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
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for dotenv-read-guard.sh
|
|
3
|
+
HOOK="$(dirname "$0")/../examples/dotenv-read-guard.sh"
|
|
4
|
+
PASS=0; FAIL=0
|
|
5
|
+
|
|
6
|
+
run_test() {
|
|
7
|
+
local desc="$1" input="$2" expect="$3"
|
|
8
|
+
result=$(echo "$input" | bash "$HOOK" 2>/dev/null; echo $?)
|
|
9
|
+
code=$(echo "$result" | tail -1)
|
|
10
|
+
if [ "$code" = "$expect" ]; then
|
|
11
|
+
echo "PASS: $desc"
|
|
12
|
+
((PASS++))
|
|
13
|
+
else
|
|
14
|
+
echo "FAIL: $desc (expected $expect, got $code)"
|
|
15
|
+
((FAIL++))
|
|
16
|
+
fi
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Should block .env files
|
|
20
|
+
run_test "Block .env" \
|
|
21
|
+
'{"tool_input":{"file_path":"/home/user/project/.env"}}' "2"
|
|
22
|
+
|
|
23
|
+
run_test "Block .env.local" \
|
|
24
|
+
'{"tool_input":{"file_path":"/app/.env.local"}}' "2"
|
|
25
|
+
|
|
26
|
+
run_test "Block .env.production" \
|
|
27
|
+
'{"tool_input":{"file_path":"/deploy/.env.production"}}' "2"
|
|
28
|
+
|
|
29
|
+
run_test "Block .env.staging" \
|
|
30
|
+
'{"tool_input":{"file_path":"/app/.env.staging"}}' "2"
|
|
31
|
+
|
|
32
|
+
run_test "Block .env.development" \
|
|
33
|
+
'{"tool_input":{"file_path":"/app/.env.development"}}' "2"
|
|
34
|
+
|
|
35
|
+
run_test "Block .env.test" \
|
|
36
|
+
'{"tool_input":{"file_path":"/project/.env.test"}}' "2"
|
|
37
|
+
|
|
38
|
+
# Should allow non-.env files
|
|
39
|
+
run_test "Allow .env.example" \
|
|
40
|
+
'{"tool_input":{"file_path":"/project/.env.example"}}' "0"
|
|
41
|
+
|
|
42
|
+
run_test "Allow README.md" \
|
|
43
|
+
'{"tool_input":{"file_path":"/project/README.md"}}' "0"
|
|
44
|
+
|
|
45
|
+
run_test "Allow package.json" \
|
|
46
|
+
'{"tool_input":{"file_path":"/project/package.json"}}' "0"
|
|
47
|
+
|
|
48
|
+
run_test "Allow config.ts" \
|
|
49
|
+
'{"tool_input":{"file_path":"/src/config.ts"}}' "0"
|
|
50
|
+
|
|
51
|
+
run_test "Allow env.ts (not dotenv)" \
|
|
52
|
+
'{"tool_input":{"file_path":"/src/env.ts"}}' "0"
|
|
53
|
+
|
|
54
|
+
run_test "Allow .envrc (direnv)" \
|
|
55
|
+
'{"tool_input":{"file_path":"/project/.envrc"}}' "0"
|
|
56
|
+
|
|
57
|
+
# Edge cases
|
|
58
|
+
run_test "Empty input" '{}' "0"
|
|
59
|
+
|
|
60
|
+
run_test "No file_path" \
|
|
61
|
+
'{"tool_input":{}}' "0"
|
|
62
|
+
|
|
63
|
+
echo ""
|
|
64
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
65
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|