cc-safe-setup 1.0.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/index.mjs +231 -0
- package/package.json +17 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
7
|
+
|
|
8
|
+
const HOME = homedir();
|
|
9
|
+
const HOOKS_DIR = join(HOME, '.claude', 'hooks');
|
|
10
|
+
const SETTINGS_PATH = join(HOME, '.claude', 'settings.json');
|
|
11
|
+
|
|
12
|
+
const c = {
|
|
13
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
14
|
+
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
|
|
15
|
+
blue: '\x1b[36m', magenta: '\x1b[35m',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════
|
|
19
|
+
// Hook definitions — each one prevents a real incident
|
|
20
|
+
// ═══════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
const HOOKS = {
|
|
23
|
+
'destructive-guard': {
|
|
24
|
+
name: 'Destructive Command Blocker',
|
|
25
|
+
why: 'A user lost their entire C:\\Users directory when rm -rf followed NTFS junctions (GitHub #36339)',
|
|
26
|
+
trigger: 'PreToolUse',
|
|
27
|
+
matcher: 'Bash',
|
|
28
|
+
script: `#!/bin/bash
|
|
29
|
+
# destructive-guard.sh — Blocks rm -rf, git reset --hard, git clean
|
|
30
|
+
INPUT=$(cat)
|
|
31
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
32
|
+
[[ -z "$COMMAND" ]] && exit 0
|
|
33
|
+
|
|
34
|
+
# rm on sensitive paths
|
|
35
|
+
if echo "$COMMAND" | grep -qE 'rm\\s+(-[rf]+\\s+)*(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|~\\/|~\\s*$|\\.\\.\\/|\\.\\.\\s*$)'; then
|
|
36
|
+
SAFE=0
|
|
37
|
+
for dir in node_modules dist build .cache __pycache__ coverage; do
|
|
38
|
+
echo "$COMMAND" | grep -qE "rm\\s+.*${dir}" && SAFE=1 && break
|
|
39
|
+
done
|
|
40
|
+
if (( SAFE == 0 )); then
|
|
41
|
+
echo "BLOCKED: rm on sensitive path. On WSL2, rm -rf follows NTFS junctions." >&2
|
|
42
|
+
exit 2
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# git reset --hard
|
|
47
|
+
echo "$COMMAND" | grep -qE 'git\\s+reset\\s+--hard' && echo "BLOCKED: git reset --hard discards uncommitted changes." >&2 && exit 2
|
|
48
|
+
|
|
49
|
+
# git clean -fd
|
|
50
|
+
echo "$COMMAND" | grep -qE 'git\\s+clean\\s+-[a-z]*[fd]' && echo "BLOCKED: git clean removes untracked files permanently. Use -n first." >&2 && exit 2
|
|
51
|
+
|
|
52
|
+
exit 0`,
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
'branch-guard': {
|
|
56
|
+
name: 'Branch Push Protector',
|
|
57
|
+
why: 'Autonomous Claude Code pushed untested code directly to main at 3am',
|
|
58
|
+
trigger: 'PreToolUse',
|
|
59
|
+
matcher: 'Bash',
|
|
60
|
+
script: `#!/bin/bash
|
|
61
|
+
# branch-guard.sh — Blocks pushes to main/master
|
|
62
|
+
INPUT=$(cat)
|
|
63
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
64
|
+
[[ -z "$COMMAND" ]] && exit 0
|
|
65
|
+
echo "$COMMAND" | grep -qE '^\\s*git\\s+push' || exit 0
|
|
66
|
+
|
|
67
|
+
PROTECTED="\${CC_PROTECT_BRANCHES:-main:master}"
|
|
68
|
+
IFS=':' read -ra BRANCHES <<< "$PROTECTED"
|
|
69
|
+
for branch in "\${BRANCHES[@]}"; do
|
|
70
|
+
if echo "$COMMAND" | grep -qwE "origin\\s+\${branch}|\${branch}\\s|\${branch}$"; then
|
|
71
|
+
echo "BLOCKED: Push to protected branch '\${branch}'. Use a feature branch + PR instead." >&2
|
|
72
|
+
exit 2
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
exit 0`,
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
'syntax-check': {
|
|
79
|
+
name: 'Post-Edit Syntax Validator',
|
|
80
|
+
why: 'A Python syntax error cascaded through 30+ files before anyone noticed',
|
|
81
|
+
trigger: 'PostToolUse',
|
|
82
|
+
matcher: 'Edit|Write',
|
|
83
|
+
script: `#!/bin/bash
|
|
84
|
+
# syntax-check.sh — Validates syntax after file edits
|
|
85
|
+
INPUT=$(cat)
|
|
86
|
+
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
|
|
87
|
+
[[ -z "$FILE" || ! -f "$FILE" ]] && exit 0
|
|
88
|
+
|
|
89
|
+
case "$FILE" in
|
|
90
|
+
*.py) python3 -m py_compile "$FILE" 2>/dev/null || { echo "SYNTAX ERROR in $FILE" >&2; exit 1; } ;;
|
|
91
|
+
*.sh) bash -n "$FILE" 2>/dev/null || { echo "SYNTAX ERROR in $FILE" >&2; exit 1; } ;;
|
|
92
|
+
*.json) python3 -c "import json; json.load(open('$FILE'))" 2>/dev/null || { echo "INVALID JSON: $FILE" >&2; exit 1; } ;;
|
|
93
|
+
*.yaml|*.yml) python3 -c "import yaml; yaml.safe_load(open('$FILE'))" 2>/dev/null || { echo "INVALID YAML: $FILE" >&2; exit 1; } ;;
|
|
94
|
+
*.js|*.mjs) node --check "$FILE" 2>/dev/null || { echo "SYNTAX ERROR in $FILE" >&2; exit 1; } ;;
|
|
95
|
+
esac
|
|
96
|
+
exit 0`,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
'context-monitor': {
|
|
100
|
+
name: 'Context Window Monitor',
|
|
101
|
+
why: 'Sessions silently lost all state at tool call 150+ with no warning',
|
|
102
|
+
trigger: 'PostToolUse',
|
|
103
|
+
matcher: '',
|
|
104
|
+
script: `#!/bin/bash
|
|
105
|
+
# context-monitor.sh — Warns when context window is filling up
|
|
106
|
+
INPUT=$(cat)
|
|
107
|
+
PERCENT=$(echo "$INPUT" | jq -r '.session.context_window.percent_used // empty' 2>/dev/null)
|
|
108
|
+
[[ -z "$PERCENT" ]] && exit 0
|
|
109
|
+
|
|
110
|
+
if (( $(echo "$PERCENT > 80" | bc -l 2>/dev/null || echo 0) )); then
|
|
111
|
+
echo "⚠️ Context window at \${PERCENT}%. Consider compacting or starting a new session." >&2
|
|
112
|
+
fi
|
|
113
|
+
exit 0`,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// ═══════════════════════════════════════════════════
|
|
118
|
+
// Installation logic
|
|
119
|
+
// ═══════════════════════════════════════════════════
|
|
120
|
+
|
|
121
|
+
function ask(question) {
|
|
122
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
123
|
+
return new Promise(resolve => {
|
|
124
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function printHeader() {
|
|
129
|
+
console.log();
|
|
130
|
+
console.log(`${c.bold} cc-safe-setup${c.reset}`);
|
|
131
|
+
console.log(`${c.dim} Make Claude Code safe for autonomous operation${c.reset}`);
|
|
132
|
+
console.log();
|
|
133
|
+
console.log(`${c.dim} This installs safety hooks that prevent real incidents:${c.reset}`);
|
|
134
|
+
console.log(`${c.red} ✗${c.reset} rm -rf deleting entire user directories (NTFS junction traversal)`);
|
|
135
|
+
console.log(`${c.red} ✗${c.reset} Untested code pushed to main at 3am`);
|
|
136
|
+
console.log(`${c.red} ✗${c.reset} Syntax errors cascading through 30+ files`);
|
|
137
|
+
console.log(`${c.red} ✗${c.reset} Sessions losing all context with no warning`);
|
|
138
|
+
console.log();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function installHook(id, hook) {
|
|
142
|
+
const hookPath = join(HOOKS_DIR, `${id}.sh`);
|
|
143
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
144
|
+
writeFileSync(hookPath, hook.script);
|
|
145
|
+
try { chmodSync(hookPath, 0o755); } catch(e) {}
|
|
146
|
+
return hookPath;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function updateSettings(installedHooks) {
|
|
150
|
+
let settings = {};
|
|
151
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
152
|
+
try {
|
|
153
|
+
settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
154
|
+
} catch(e) {
|
|
155
|
+
console.log(`${c.yellow} Warning: Could not parse existing settings.json. Creating backup.${c.reset}`);
|
|
156
|
+
writeFileSync(SETTINGS_PATH + '.bak', readFileSync(SETTINGS_PATH));
|
|
157
|
+
settings = {};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!settings.hooks) settings.hooks = {};
|
|
162
|
+
|
|
163
|
+
for (const [id, hook] of Object.entries(installedHooks)) {
|
|
164
|
+
const trigger = hook.trigger;
|
|
165
|
+
if (!settings.hooks[trigger]) settings.hooks[trigger] = [];
|
|
166
|
+
|
|
167
|
+
const hookPath = join(HOOKS_DIR, `${id}.sh`);
|
|
168
|
+
const entry = {
|
|
169
|
+
matcher: hook.matcher,
|
|
170
|
+
hooks: [{ type: 'command', command: hookPath }]
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Check if already exists
|
|
174
|
+
const exists = settings.hooks[trigger].some(e =>
|
|
175
|
+
e.hooks && e.hooks.some(h => h.command && h.command.includes(`${id}.sh`))
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!exists) {
|
|
179
|
+
settings.hooks[trigger].push(entry);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
|
|
184
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function main() {
|
|
188
|
+
printHeader();
|
|
189
|
+
|
|
190
|
+
// Show what will be installed
|
|
191
|
+
console.log(`${c.bold} Hooks to install:${c.reset}`);
|
|
192
|
+
console.log();
|
|
193
|
+
for (const [id, hook] of Object.entries(HOOKS)) {
|
|
194
|
+
console.log(` ${c.green}●${c.reset} ${c.bold}${hook.name}${c.reset}`);
|
|
195
|
+
console.log(` ${c.dim}${hook.why}${c.reset}`);
|
|
196
|
+
}
|
|
197
|
+
console.log();
|
|
198
|
+
|
|
199
|
+
const answer = await ask(` Install all ${Object.keys(HOOKS).length} safety hooks? [Y/n] `);
|
|
200
|
+
if (answer.toLowerCase() === 'n') {
|
|
201
|
+
console.log(`\n ${c.dim}Cancelled. No changes made.${c.reset}\n`);
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log();
|
|
206
|
+
|
|
207
|
+
// Install hooks
|
|
208
|
+
const installed = {};
|
|
209
|
+
for (const [id, hook] of Object.entries(HOOKS)) {
|
|
210
|
+
const path = installHook(id, hook);
|
|
211
|
+
installed[id] = hook;
|
|
212
|
+
console.log(` ${c.green}✓${c.reset} ${hook.name} → ${c.dim}${path}${c.reset}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Update settings.json
|
|
216
|
+
updateSettings(installed);
|
|
217
|
+
console.log(` ${c.green}✓${c.reset} settings.json updated → ${c.dim}${SETTINGS_PATH}${c.reset}`);
|
|
218
|
+
|
|
219
|
+
console.log();
|
|
220
|
+
console.log(`${c.bold} Done.${c.reset} ${Object.keys(HOOKS).length} safety hooks installed.`);
|
|
221
|
+
console.log();
|
|
222
|
+
console.log(` ${c.dim}Restart Claude Code to activate the hooks.${c.reset}`);
|
|
223
|
+
console.log();
|
|
224
|
+
console.log(` ${c.dim}Verify your setup:${c.reset} ${c.blue}npx cc-health-check${c.reset}`);
|
|
225
|
+
console.log();
|
|
226
|
+
console.log(` ${c.dim}Want more hooks + templates + tools?${c.reset}`);
|
|
227
|
+
console.log(` ${c.bold}https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=npm&utm_medium=cli&utm_campaign=safe-setup${c.reset}`);
|
|
228
|
+
console.log();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-safe-setup",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "One command to make Claude Code safe for autonomous operation. Installs destructive command blockers, branch guards, and syntax checks.",
|
|
5
|
+
"main": "index.mjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cc-safe-setup": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["claude-code", "ai", "safety", "hooks", "autonomous", "cli"],
|
|
10
|
+
"author": "yurukusa",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/yurukusa/cc-safe-setup"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/yurukusa/cc-safe-setup"
|
|
17
|
+
}
|