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.
Files changed (2) hide show
  1. package/index.mjs +231 -0
  2. 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
+ }