cc-safe-setup 1.1.0 → 1.1.2
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/.github/workflows/test.yml +14 -0
- package/README.md +33 -23
- package/demo.svg +1 -0
- package/package.json +15 -2
- package/scripts.json +1 -1
- package/test.sh +135 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
steps:
|
|
7
|
+
- uses: actions/checkout@v4
|
|
8
|
+
- uses: actions/setup-node@v4
|
|
9
|
+
with:
|
|
10
|
+
node-version: '20'
|
|
11
|
+
- run: sudo apt-get install -y jq
|
|
12
|
+
- run: node index.mjs --help
|
|
13
|
+
- run: node index.mjs --dry-run
|
|
14
|
+
- run: bash test.sh
|
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/cc-safe-setup)
|
|
4
4
|
[](https://www.npmjs.com/package/cc-safe-setup)
|
|
5
|
+
[](https://github.com/yurukusa/cc-safe-setup/actions/workflows/test.yml)
|
|
5
6
|
|
|
6
7
|
**One command to make Claude Code safe for autonomous operation.**
|
|
7
8
|
|
|
@@ -9,7 +10,9 @@
|
|
|
9
10
|
npx cc-safe-setup
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
Installs
|
|
13
|
+
Installs 7 production-tested safety hooks in ~10 seconds. Zero dependencies. No manual configuration.
|
|
14
|
+
|
|
15
|
+
<img src="demo.svg" alt="cc-safe-setup demo" width="600">
|
|
13
16
|
|
|
14
17
|
```
|
|
15
18
|
cc-safe-setup
|
|
@@ -18,6 +21,8 @@ Installs 6 production-tested safety hooks in ~10 seconds. Zero dependencies. No
|
|
|
18
21
|
Prevents real incidents:
|
|
19
22
|
✗ rm -rf deleting entire user directories (NTFS junction traversal)
|
|
20
23
|
✗ Untested code pushed to main at 3am
|
|
24
|
+
✗ Force-push rewriting shared branch history
|
|
25
|
+
✗ API keys committed to public repos via git add .
|
|
21
26
|
✗ Syntax errors cascading through 30+ files
|
|
22
27
|
✗ Sessions losing all context with no warning
|
|
23
28
|
|
|
@@ -29,36 +34,30 @@ Installs 6 production-tested safety hooks in ~10 seconds. Zero dependencies. No
|
|
|
29
34
|
● Context Window Monitor
|
|
30
35
|
● Bash Comment Stripper
|
|
31
36
|
● cd+git Auto-Approver
|
|
37
|
+
● Secret Leak Prevention
|
|
32
38
|
|
|
33
|
-
Install all
|
|
34
|
-
|
|
35
|
-
✓ Destructive Command Blocker
|
|
36
|
-
✓ Branch Push Protector
|
|
37
|
-
✓ Post-Edit Syntax Validator
|
|
38
|
-
✓ Context Window Monitor
|
|
39
|
-
✓ Bash Comment Stripper
|
|
40
|
-
✓ cd+git Auto-Approver
|
|
41
|
-
✓ settings.json updated
|
|
39
|
+
Install all 7 safety hooks? [Y/n] Y
|
|
42
40
|
|
|
43
|
-
Done.
|
|
41
|
+
✓ Done. 7 safety hooks installed.
|
|
44
42
|
```
|
|
45
43
|
|
|
46
44
|
## Why This Exists
|
|
47
45
|
|
|
48
|
-
A Claude Code user [lost their entire C:\Users directory](https://github.com/anthropics/claude-code/issues/36339) when `rm -rf` followed NTFS junctions. Another had untested code pushed to main at 3am. Syntax errors cascaded through 30+ files before anyone noticed.
|
|
46
|
+
A Claude Code user [lost their entire C:\Users directory](https://github.com/anthropics/claude-code/issues/36339) when `rm -rf` followed NTFS junctions. Another had untested code pushed to main at 3am. API keys got committed via `git add .`. Syntax errors cascaded through 30+ files before anyone noticed.
|
|
49
47
|
|
|
50
48
|
Claude Code ships with no safety hooks by default. This tool fixes that.
|
|
51
49
|
|
|
52
50
|
## What Gets Installed
|
|
53
51
|
|
|
54
|
-
| Hook | Prevents |
|
|
55
|
-
|
|
56
|
-
| **Destructive Guard** | `rm -rf /`, `git reset --hard`, `git clean -fd
|
|
57
|
-
| **Branch Guard** |
|
|
58
|
-
| **
|
|
59
|
-
| **
|
|
60
|
-
| **
|
|
61
|
-
| **
|
|
52
|
+
| Hook | Prevents | Related Issues |
|
|
53
|
+
|------|----------|----------------|
|
|
54
|
+
| **Destructive Guard** | `rm -rf /`, `git reset --hard`, `git clean -fd`, NFS mount detection | [#36339](https://github.com/anthropics/claude-code/issues/36339) [#36640](https://github.com/anthropics/claude-code/issues/36640) |
|
|
55
|
+
| **Branch Guard** | Pushes to main/master + force-push (`--force`) on all branches | |
|
|
56
|
+
| **Secret Guard** | `git add .env`, credential files, `git add .` with .env present | [#6527](https://github.com/anthropics/claude-code/issues/6527) |
|
|
57
|
+
| **Syntax Check** | Python, Shell, JSON, YAML, JS errors after edits | |
|
|
58
|
+
| **Context Monitor** | Session state loss from context window overflow (40%→25%→20%→15% warnings) | |
|
|
59
|
+
| **Comment Stripper** | Bash comments breaking permission allowlists | [#29582](https://github.com/anthropics/claude-code/issues/29582) |
|
|
60
|
+
| **cd+git Auto-Approver** | Permission prompt spam for `cd /path && git log` | [#32985](https://github.com/anthropics/claude-code/issues/32985) [#16561](https://github.com/anthropics/claude-code/issues/16561) |
|
|
62
61
|
|
|
63
62
|
Each hook exists because a real incident happened without it.
|
|
64
63
|
|
|
@@ -72,10 +71,21 @@ Safe to run multiple times. Existing settings are preserved. A backup is created
|
|
|
72
71
|
|
|
73
72
|
**Preview first:** `npx cc-safe-setup --dry-run`
|
|
74
73
|
|
|
75
|
-
**Uninstall:** `npx cc-safe-setup --uninstall` — removes all
|
|
74
|
+
**Uninstall:** `npx cc-safe-setup --uninstall` — removes all hooks and cleans settings.json.
|
|
76
75
|
|
|
77
76
|
**Requires:** [jq](https://jqlang.github.io/jq/) for JSON parsing (`brew install jq` / `apt install jq`).
|
|
78
77
|
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
| Variable | Hook | Default |
|
|
81
|
+
|----------|------|---------|
|
|
82
|
+
| `CC_ALLOW_DESTRUCTIVE=1` | destructive-guard | `0` (protection on) |
|
|
83
|
+
| `CC_SAFE_DELETE_DIRS` | destructive-guard | `node_modules:dist:build:.cache:__pycache__:coverage` |
|
|
84
|
+
| `CC_PROTECT_BRANCHES` | branch-guard | `main:master` |
|
|
85
|
+
| `CC_ALLOW_FORCE_PUSH=1` | branch-guard | `0` (protection on) |
|
|
86
|
+
| `CC_SECRET_PATTERNS` | secret-guard | `.env:.env.local:credentials:*.pem:*.key` |
|
|
87
|
+
| `CC_CONTEXT_MISSION_FILE` | context-monitor | `$HOME/mission.md` |
|
|
88
|
+
|
|
79
89
|
## After Installing
|
|
80
90
|
|
|
81
91
|
Verify your setup:
|
|
@@ -86,9 +96,9 @@ npx cc-health-check
|
|
|
86
96
|
|
|
87
97
|
## Full Kit
|
|
88
98
|
|
|
89
|
-
cc-safe-setup gives you
|
|
99
|
+
cc-safe-setup gives you 7 essential hooks. For the complete autonomous operation toolkit:
|
|
90
100
|
|
|
91
|
-
**[Claude Code Ops Kit](https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=github&utm_medium=readme&utm_campaign=safe-setup)**
|
|
101
|
+
**[Claude Code Ops Kit](https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=github&utm_medium=readme&utm_campaign=safe-setup)** — 15 hooks + 6 templates + 3 exclusive tools + install.sh. Production-ready in 15 minutes.
|
|
92
102
|
|
|
93
103
|
Or start with the free hooks: [claude-code-hooks](https://github.com/yurukusa/claude-code-hooks)
|
|
94
104
|
|
package/demo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="840" height="581.04"><rect width="840" height="581.04" rx="5" ry="5" class="a"/><svg y="0%" x="0%"><circle cx="20" cy="20" r="6" fill="#ff5f58"/><circle cx="40" cy="20" r="6" fill="#ffbd2e"/><circle cx="60" cy="20" r="6" fill="#18c132"/></svg><svg height="521.04" viewBox="0 0 80 52.104" width="800" x="15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="50"><style>@keyframes l{0%{transform:translateX(0)}.1%{transform:translateX(-80px)}16.9%{transform:translateX(-400px)}17%{transform:translateX(-2400px)}50%{transform:translateX(-2480px)}66.9%{transform:translateX(-2720px)}99.8%{transform:translateX(-2800px)}to{transform:translateX(-2880px)}}.a{fill:#282d35}.f{fill:#b9c0cb}.f,.g,.h,.i,.j{white-space:pre}.g{fill:#b9c0cb;font-weight:700}.h,.i,.j{fill:#e88388}.i,.j{fill:#a8cc8c}.j{fill:#dbab79}</style><g font-family="Monaco,Consolas,Menlo,'Bitstream Vera Sans Mono','Powerline Symbols',monospace" font-size="1.67"><defs><symbol id="1"><text y="1.67" class="f">$</text><text x="2.004" y="1.67" class="f">npx</text><text x="6.012" y="1.67" class="f">cc-safe-setup</text><text x="20.04" y="1.67" class="f">--dry-run</text></symbol><symbol id="2"><text x="2.004" y="1.67" class="g">cc-safe-setup</text></symbol><symbol id="3"><text x="2.004" y="1.67" class="f">Make</text><text x="7.014" y="1.67" class="f">Claude</text><text x="14.028" y="1.67" class="f">Code</text><text x="19.038" y="1.67" class="f">safe</text><text x="24.048" y="1.67" class="f">for</text><text x="28.056" y="1.67" class="f">autonomous</text><text x="39.078" y="1.67" class="f">operation</text></symbol><symbol id="4"><text x="2.004" y="1.67" class="f">Prevents</text><text x="11.022" y="1.67" class="f">real</text><text x="16.032" y="1.67" class="f">incidents:</text></symbol><symbol id="5"><text x="2.004" y="1.67" class="h">x</text><text x="4.008" y="1.67" class="f">rm</text><text x="7.014" y="1.67" class="f">-rf</text><text x="11.022" y="1.67" class="f">deleting</text><text x="20.04" y="1.67" class="f">entire</text><text x="27.054" y="1.67" class="f">user</text><text x="32.064" y="1.67" class="f">directories</text><text x="44.088" y="1.67" class="f">(NTFS</text><text x="50.1" y="1.67" class="f">junction</text><text x="59.118" y="1.67" class="f">traversal)</text></symbol><symbol id="6"><text x="2.004" y="1.67" class="h">x</text><text x="4.008" y="1.67" class="f">Untested</text><text x="13.026" y="1.67" class="f">code</text><text x="18.036" y="1.67" class="f">pushed</text><text x="25.05" y="1.67" class="f">to</text><text x="28.056" y="1.67" class="f">main</text><text x="33.066" y="1.67" class="f">at</text><text x="36.072" y="1.67" class="f">3am</text></symbol><symbol id="7"><text x="2.004" y="1.67" class="h">x</text><text x="4.008" y="1.67" class="f">Force-push</text><text x="15.03" y="1.67" class="f">rewriting</text><text x="25.05" y="1.67" class="f">shared</text><text x="32.064" y="1.67" class="f">branch</text><text x="39.078" y="1.67" class="f">history</text></symbol><symbol id="8"><text x="2.004" y="1.67" class="h">x</text><text x="4.008" y="1.67" class="f">API</text><text x="8.016" y="1.67" class="f">keys</text><text x="13.026" y="1.67" class="f">committed</text><text x="23.046" y="1.67" class="f">to</text><text x="26.052" y="1.67" class="f">public</text><text x="33.066" y="1.67" class="f">repos</text><text x="39.078" y="1.67" class="f">via</text><text x="43.086" y="1.67" class="f">git</text><text x="47.094" y="1.67" class="f">add</text><text x="51.102" y="1.67" class="f">.</text></symbol><symbol id="9"><text x="2.004" y="1.67" class="h">x</text><text x="4.008" y="1.67" class="f">Syntax</text><text x="11.022" y="1.67" class="f">errors</text><text x="18.036" y="1.67" class="f">cascading</text><text x="28.056" y="1.67" class="f">through</text><text x="36.072" y="1.67" class="f">30+</text><text x="40.08" y="1.67" class="f">files</text></symbol><symbol id="10"><text x="2.004" y="1.67" class="h">x</text><text x="4.008" y="1.67" class="f">Sessions</text><text x="13.026" y="1.67" class="f">losing</text><text x="20.04" y="1.67" class="f">all</text><text x="24.048" y="1.67" class="f">context</text><text x="32.064" y="1.67" class="f">with</text><text x="37.074" y="1.67" class="f">no</text><text x="40.08" y="1.67" class="f">warning</text></symbol><symbol id="11"><text x="2.004" y="1.67" class="g">Hooks</text><text x="8.016" y="1.67" class="g">to</text><text x="11.022" y="1.67" class="g">install:</text></symbol><symbol id="12"><text x="2.004" y="1.67" class="i">*</text><text x="4.008" y="1.67" class="g">Destructive</text><text x="16.032" y="1.67" class="g">Command</text><text x="24.048" y="1.67" class="g">Blocker</text></symbol><symbol id="13"><text x="4.008" y="1.67" class="f">A</text><text x="6.012" y="1.67" class="f">user</text><text x="11.022" y="1.67" class="f">lost</text><text x="16.032" y="1.67" class="f">their</text><text x="22.044" y="1.67" class="f">entire</text><text x="29.058" y="1.67" class="f">C:\Users</text><text x="38.076" y="1.67" class="f">directory</text><text x="48.096" y="1.67" class="f">when</text><text x="53.106" y="1.67" class="f">rm</text><text x="56.112" y="1.67" class="f">-rf</text><text x="60.12" y="1.67" class="f">followed</text><text x="69.138" y="1.67" class="f">NTFS</text><text x="74.148" y="1.67" class="f">juncti</text></symbol><symbol id="14"><text y="1.67" class="f">ons</text></symbol><symbol id="15"><text x="2.004" y="1.67" class="i">*</text><text x="4.008" y="1.67" class="g">Branch</text><text x="11.022" y="1.67" class="g">Push</text><text x="16.032" y="1.67" class="g">Protector</text></symbol><symbol id="16"><text x="4.008" y="1.67" class="f">Autonomous</text><text x="15.03" y="1.67" class="f">Claude</text><text x="22.044" y="1.67" class="f">Code</text><text x="27.054" y="1.67" class="f">pushed</text><text x="34.068" y="1.67" class="f">untested</text><text x="43.086" y="1.67" class="f">code</text><text x="48.096" y="1.67" class="f">directly</text><text x="57.114" y="1.67" class="f">to</text><text x="60.12" y="1.67" class="f">main</text><text x="65.13" y="1.67" class="f">at</text><text x="68.136" y="1.67" class="f">3am</text></symbol><symbol id="17"><text x="2.004" y="1.67" class="i">*</text><text x="4.008" y="1.67" class="g">Post-Edit</text><text x="14.028" y="1.67" class="g">Syntax</text><text x="21.042" y="1.67" class="g">Validator</text></symbol><symbol id="18"><text x="4.008" y="1.67" class="f">A</text><text x="6.012" y="1.67" class="f">Python</text><text x="13.026" y="1.67" class="f">syntax</text><text x="20.04" y="1.67" class="f">error</text><text x="26.052" y="1.67" class="f">cascaded</text><text x="35.07" y="1.67" class="f">through</text><text x="43.086" y="1.67" class="f">30+</text><text x="47.094" y="1.67" class="f">files</text><text x="53.106" y="1.67" class="f">before</text><text x="60.12" y="1.67" class="f">anyone</text><text x="67.134" y="1.67" class="f">noticed</text></symbol><symbol id="19"><text x="2.004" y="1.67" class="i">*</text><text x="4.008" y="1.67" class="g">Context</text><text x="12.024" y="1.67" class="g">Window</text><text x="19.038" y="1.67" class="g">Monitor</text></symbol><symbol id="20"><text x="4.008" y="1.67" class="f">Sessions</text><text x="13.026" y="1.67" class="f">silently</text><text x="22.044" y="1.67" class="f">lost</text><text x="27.054" y="1.67" class="f">all</text><text x="31.062" y="1.67" class="f">state</text><text x="37.074" y="1.67" class="f">at</text><text x="40.08" y="1.67" class="f">tool</text><text x="45.09" y="1.67" class="f">call</text><text x="50.1" y="1.67" class="f">150+</text><text x="55.11" y="1.67" class="f">with</text><text x="60.12" y="1.67" class="f">no</text><text x="63.126" y="1.67" class="f">warning</text></symbol><symbol id="21"><text x="2.004" y="1.67" class="i">*</text><text x="4.008" y="1.67" class="g">Bash</text><text x="9.018" y="1.67" class="g">Comment</text><text x="17.034" y="1.67" class="g">Stripper</text></symbol><symbol id="22"><text x="4.008" y="1.67" class="f">Comments</text><text x="13.026" y="1.67" class="f">in</text><text x="16.032" y="1.67" class="f">bash</text><text x="21.042" y="1.67" class="f">commands</text><text x="30.06" y="1.67" class="f">break</text><text x="36.072" y="1.67" class="f">permission</text><text x="47.094" y="1.67" class="f">allowlists</text><text x="58.116" y="1.67" class="f">(18</text><text x="62.124" y="1.67" class="f">reactions</text><text x="72.144" y="1.67" class="f">on</text><text x="75.15" y="1.67" class="f">GitHu</text></symbol><symbol id="23"><text y="1.67" class="f">b</text><text x="2.004" y="1.67" class="f">#29582)</text></symbol><symbol id="24"><text x="2.004" y="1.67" class="i">*</text><text x="4.008" y="1.67" class="g">cd+git</text><text x="11.022" y="1.67" class="g">Auto-Approver</text></symbol><symbol id="25"><text x="4.008" y="1.67" class="f">cd+git</text><text x="11.022" y="1.67" class="f">compounds</text><text x="21.042" y="1.67" class="f">spam</text><text x="26.052" y="1.67" class="f">permission</text><text x="37.074" y="1.67" class="f">prompts</text><text x="45.09" y="1.67" class="f">for</text><text x="49.098" y="1.67" class="f">read-only</text><text x="59.118" y="1.67" class="f">operations</text><text x="70.14" y="1.67" class="f">(9</text><text x="73.146" y="1.67" class="f">reactio</text></symbol><symbol id="26"><text y="1.67" class="f">ns</text><text x="3.006" y="1.67" class="f">on</text><text x="6.012" y="1.67" class="f">#32985)</text></symbol><symbol id="27"><text x="2.004" y="1.67" class="i">*</text><text x="4.008" y="1.67" class="g">Secret</text><text x="11.022" y="1.67" class="g">Leak</text><text x="16.032" y="1.67" class="g">Prevention</text></symbol><symbol id="28"><text x="4.008" y="1.67" class="f">git</text><text x="8.016" y="1.67" class="f">add</text><text x="12.024" y="1.67" class="f">.env</text><text x="17.034" y="1.67" class="f">accidentally</text><text x="30.06" y="1.67" class="f">committed</text><text x="40.08" y="1.67" class="f">API</text><text x="44.088" y="1.67" class="f">keys</text><text x="49.098" y="1.67" class="f">to</text><text x="52.104" y="1.67" class="f">a</text><text x="54.108" y="1.67" class="f">public</text><text x="61.122" y="1.67" class="f">repo</text></symbol><symbol id="29"><text x="2.004" y="1.67" class="j">--dry-run:</text><text x="13.026" y="1.67" class="j">showing</text><text x="21.042" y="1.67" class="j">what</text><text x="26.052" y="1.67" class="j">would</text><text x="32.064" y="1.67" class="j">be</text><text x="35.07" y="1.67" class="j">installed</text><text x="45.09" y="1.67" class="j">(no</text><text x="49.098" y="1.67" class="j">changes</text><text x="57.114" y="1.67" class="j">made)</text></symbol><symbol id="30"><text x="2.004" y="1.67" class="f">would</text><text x="8.016" y="1.67" class="f">install:</text><text x="17.034" y="1.67" class="f">/home/namakusa/.claude/hooks/destructive-guard.sh</text></symbol><symbol id="31"><text x="2.004" y="1.67" class="f">would</text><text x="8.016" y="1.67" class="f">install:</text><text x="17.034" y="1.67" class="f">/home/namakusa/.claude/hooks/branch-guard.sh</text></symbol><symbol id="32"><text x="2.004" y="1.67" class="f">would</text><text x="8.016" y="1.67" class="f">install:</text><text x="17.034" y="1.67" class="f">/home/namakusa/.claude/hooks/syntax-check.sh</text></symbol><symbol id="33"><text x="2.004" y="1.67" class="f">would</text><text x="8.016" y="1.67" class="f">install:</text><text x="17.034" y="1.67" class="f">/home/namakusa/.claude/hooks/context-monitor.sh</text></symbol><symbol id="34"><text x="2.004" y="1.67" class="f">would</text><text x="8.016" y="1.67" class="f">install:</text><text x="17.034" y="1.67" class="f">/home/namakusa/.claude/hooks/comment-strip.sh</text></symbol><symbol id="35"><text x="2.004" y="1.67" class="f">would</text><text x="8.016" y="1.67" class="f">install:</text><text x="17.034" y="1.67" class="f">/home/namakusa/.claude/hooks/cd-git-allow.sh</text></symbol><symbol id="36"><text x="2.004" y="1.67" class="f">would</text><text x="8.016" y="1.67" class="f">install:</text><text x="17.034" y="1.67" class="f">/home/namakusa/.claude/hooks/secret-guard.sh</text></symbol><symbol id="37"><text x="2.004" y="1.67" class="f">would</text><text x="8.016" y="1.67" class="f">update:</text><text x="16.032" y="1.67" class="f">/home/namakusa/.claude/settings.json</text></symbol><symbol id="38"><text y="1.67" class="f">$</text><text x="2.004" y="1.67" class="f">#</text><text x="4.008" y="1.67" class="f">Test:</text><text x="10.02" y="1.67" class="f">destructive</text><text x="22.044" y="1.67" class="f">command</text><text x="30.06" y="1.67" class="f">is</text><text x="33.066" y="1.67" class="f">blocked</text></symbol><symbol id="39"><text y="1.67" class="f">BLOCKED:</text><text x="9.018" y="1.67" class="f">Target</text><text x="16.032" y="1.67" class="f">contains</text><text x="25.05" y="1.67" class="f">a</text><text x="27.054" y="1.67" class="f">mounted</text><text x="35.07" y="1.67" class="f">filesystem</text><text x="46.092" y="1.67" class="f">(NFS,</text><text x="52.104" y="1.67" class="f">Docker,</text><text x="60.12" y="1.67" class="f">bind).</text></symbol><symbol id="40"><text y="1.67" class="f">Command:</text><text x="9.018" y="1.67" class="f">rm</text><text x="12.024" y="1.67" class="f">-rf</text><text x="16.032" y="1.67" class="f">/</text></symbol><symbol id="41"><text y="1.67" class="f">Unmount</text><text x="8.016" y="1.67" class="f">the</text><text x="12.024" y="1.67" class="f">filesystem</text><text x="23.046" y="1.67" class="f">first,</text><text x="30.06" y="1.67" class="f">then</text><text x="35.07" y="1.67" class="f">retry.</text></symbol><symbol id="42"><text y="1.67" class="f">Exit</text><text x="5.01" y="1.67" class="f">code:</text><text x="11.022" y="1.67" class="f">2</text><text x="13.026" y="1.67" class="f">(blocked)</text></symbol><symbol id="43"><text y="1.67" class="f">$</text><text x="2.004" y="1.67" class="f">#</text><text x="4.008" y="1.67" class="f">Test:</text><text x="10.02" y="1.67" class="f">safe</text><text x="15.03" y="1.67" class="f">command</text><text x="23.046" y="1.67" class="f">passes</text><text x="30.06" y="1.67" class="f">through</text></symbol><symbol id="a"><path fill="transparent" d="M0 0h80v25H0z"/></symbol></defs><path class="a" d="M0 0h80v52.104H0z"/><g style="animation-duration:6.076617s;animation-iteration-count:infinite;animation-name:l;animation-timing-function:steps(1,end)"><svg width="2960"><svg><use xlink:href="#a"/></svg><svg x="80"><use xlink:href="#a"/><use xlink:href="#1"/></svg><svg x="160"><use xlink:href="#a"/><use xlink:href="#1"/></svg><svg x="240"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/></svg><svg x="320"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/></svg><svg x="400"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/></svg><svg x="480"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/></svg><svg x="560"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/></svg><svg x="640"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/></svg><svg x="720"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/></svg><svg x="800"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/></svg><svg x="880"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/><use xlink:href="#10" y="23.881"/></svg><svg x="960"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/><use xlink:href="#10" y="23.881"/><use xlink:href="#11" y="28.223"/></svg><svg x="1040"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/><use xlink:href="#10" y="23.881"/><use xlink:href="#11" y="28.223"/></svg><svg x="1120"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/><use xlink:href="#10" y="23.881"/><use xlink:href="#11" y="28.223"/><use xlink:href="#12" y="32.565"/><use xlink:href="#13" y="34.736"/><use xlink:href="#14" y="36.907"/></svg><svg x="1200"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/><use xlink:href="#10" y="23.881"/><use xlink:href="#11" y="28.223"/><use xlink:href="#12" y="32.565"/><use xlink:href="#13" y="34.736"/><use xlink:href="#14" y="36.907"/><use xlink:href="#15" y="39.078"/><use xlink:href="#16" y="41.249"/></svg><svg x="1280"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/><use xlink:href="#10" y="23.881"/><use xlink:href="#11" y="28.223"/><use xlink:href="#12" y="32.565"/><use xlink:href="#13" y="34.736"/><use xlink:href="#14" y="36.907"/><use xlink:href="#15" y="39.078"/><use xlink:href="#16" y="41.249"/><use xlink:href="#17" y="43.42"/></svg><svg x="1360"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/><use xlink:href="#10" y="23.881"/><use xlink:href="#11" y="28.223"/><use xlink:href="#12" y="32.565"/><use xlink:href="#13" y="34.736"/><use xlink:href="#14" y="36.907"/><use xlink:href="#15" y="39.078"/><use xlink:href="#16" y="41.249"/><use xlink:href="#17" y="43.42"/><use xlink:href="#18" y="45.591"/><use xlink:href="#19" y="47.762"/></svg><svg x="1440"><use xlink:href="#a"/><use xlink:href="#1"/><use xlink:href="#2" y="4.342"/><use xlink:href="#3" y="6.513"/><use xlink:href="#4" y="10.855"/><use xlink:href="#5" y="13.026"/><use xlink:href="#6" y="15.197"/><use xlink:href="#7" y="17.368"/><use xlink:href="#8" y="19.539"/><use xlink:href="#9" y="21.71"/><use xlink:href="#10" y="23.881"/><use xlink:href="#11" y="28.223"/><use xlink:href="#12" y="32.565"/><use xlink:href="#13" y="34.736"/><use xlink:href="#14" y="36.907"/><use xlink:href="#15" y="39.078"/><use xlink:href="#16" y="41.249"/><use xlink:href="#17" y="43.42"/><use xlink:href="#18" y="45.591"/><use xlink:href="#19" y="47.762"/><use xlink:href="#20" y="49.933"/></svg><svg x="1520"><use xlink:href="#a"/><use xlink:href="#2" y="2.171"/><use xlink:href="#3" y="4.342"/><use xlink:href="#4" y="8.684"/><use xlink:href="#5" y="10.855"/><use xlink:href="#6" y="13.026"/><use xlink:href="#7" y="15.197"/><use xlink:href="#8" y="17.368"/><use xlink:href="#9" y="19.539"/><use xlink:href="#10" y="21.71"/><use xlink:href="#11" y="26.052"/><use xlink:href="#12" y="30.394"/><use xlink:href="#13" y="32.565"/><use xlink:href="#14" y="34.736"/><use xlink:href="#15" y="36.907"/><use xlink:href="#16" y="39.078"/><use xlink:href="#17" y="41.249"/><use xlink:href="#18" y="43.42"/><use xlink:href="#19" y="45.591"/><use xlink:href="#20" y="47.762"/><use xlink:href="#21" y="49.933"/></svg><svg x="1600"><use xlink:href="#a"/><use xlink:href="#5"/><use xlink:href="#6" y="2.171"/><use xlink:href="#7" y="4.342"/><use xlink:href="#8" y="6.513"/><use xlink:href="#9" y="8.684"/><use xlink:href="#10" y="10.855"/><use xlink:href="#11" y="15.197"/><use xlink:href="#12" y="19.539"/><use xlink:href="#13" y="21.71"/><use xlink:href="#14" y="23.881"/><use xlink:href="#15" y="26.052"/><use xlink:href="#16" y="28.223"/><use xlink:href="#17" y="30.394"/><use xlink:href="#18" y="32.565"/><use xlink:href="#19" y="34.736"/><use xlink:href="#20" y="36.907"/><use xlink:href="#21" y="39.078"/><use xlink:href="#22" y="41.249"/><use xlink:href="#23" y="43.42"/><use xlink:href="#24" y="45.591"/><use xlink:href="#25" y="47.762"/><use xlink:href="#26" y="49.933"/></svg><svg x="1680"><use xlink:href="#a"/><use xlink:href="#7"/><use xlink:href="#8" y="2.171"/><use xlink:href="#9" y="4.342"/><use xlink:href="#10" y="6.513"/><use xlink:href="#11" y="10.855"/><use xlink:href="#12" y="15.197"/><use xlink:href="#13" y="17.368"/><use xlink:href="#14" y="19.539"/><use xlink:href="#15" y="21.71"/><use xlink:href="#16" y="23.881"/><use xlink:href="#17" y="26.052"/><use xlink:href="#18" y="28.223"/><use xlink:href="#19" y="30.394"/><use xlink:href="#20" y="32.565"/><use xlink:href="#21" y="34.736"/><use xlink:href="#22" y="36.907"/><use xlink:href="#23" y="39.078"/><use xlink:href="#24" y="41.249"/><use xlink:href="#25" y="43.42"/><use xlink:href="#26" y="45.591"/><use xlink:href="#27" y="47.762"/><use xlink:href="#28" y="49.933"/></svg><svg x="1760"><use xlink:href="#a"/><use xlink:href="#10"/><use xlink:href="#11" y="4.342"/><use xlink:href="#12" y="8.684"/><use xlink:href="#13" y="10.855"/><use xlink:href="#14" y="13.026"/><use xlink:href="#15" y="15.197"/><use xlink:href="#16" y="17.368"/><use xlink:href="#17" y="19.539"/><use xlink:href="#18" y="21.71"/><use xlink:href="#19" y="23.881"/><use xlink:href="#20" y="26.052"/><use xlink:href="#21" y="28.223"/><use xlink:href="#22" y="30.394"/><use xlink:href="#23" y="32.565"/><use xlink:href="#24" y="34.736"/><use xlink:href="#25" y="36.907"/><use xlink:href="#26" y="39.078"/><use xlink:href="#27" y="41.249"/><use xlink:href="#28" y="43.42"/><use xlink:href="#29" y="47.762"/></svg><svg x="1840"><use xlink:href="#a"/><use xlink:href="#11" y="2.171"/><use xlink:href="#12" y="6.513"/><use xlink:href="#13" y="8.684"/><use xlink:href="#14" y="10.855"/><use xlink:href="#15" y="13.026"/><use xlink:href="#16" y="15.197"/><use xlink:href="#17" y="17.368"/><use xlink:href="#18" y="19.539"/><use xlink:href="#19" y="21.71"/><use xlink:href="#20" y="23.881"/><use xlink:href="#21" y="26.052"/><use xlink:href="#22" y="28.223"/><use xlink:href="#23" y="30.394"/><use xlink:href="#24" y="32.565"/><use xlink:href="#25" y="34.736"/><use xlink:href="#26" y="36.907"/><use xlink:href="#27" y="39.078"/><use xlink:href="#28" y="41.249"/><use xlink:href="#29" y="45.591"/><use xlink:href="#30" y="49.933"/></svg><svg x="1920"><use xlink:href="#a"/><use xlink:href="#11"/><use xlink:href="#12" y="4.342"/><use xlink:href="#13" y="6.513"/><use xlink:href="#14" y="8.684"/><use xlink:href="#15" y="10.855"/><use xlink:href="#16" y="13.026"/><use xlink:href="#17" y="15.197"/><use xlink:href="#18" y="17.368"/><use xlink:href="#19" y="19.539"/><use xlink:href="#20" y="21.71"/><use xlink:href="#21" y="23.881"/><use xlink:href="#22" y="26.052"/><use xlink:href="#23" y="28.223"/><use xlink:href="#24" y="30.394"/><use xlink:href="#25" y="32.565"/><use xlink:href="#26" y="34.736"/><use xlink:href="#27" y="36.907"/><use xlink:href="#28" y="39.078"/><use xlink:href="#29" y="43.42"/><use xlink:href="#30" y="47.762"/><use xlink:href="#31" y="49.933"/></svg><svg x="2000"><use xlink:href="#a"/><use xlink:href="#12"/><use xlink:href="#13" y="2.171"/><use xlink:href="#14" y="4.342"/><use xlink:href="#15" y="6.513"/><use xlink:href="#16" y="8.684"/><use xlink:href="#17" y="10.855"/><use xlink:href="#18" y="13.026"/><use xlink:href="#19" y="15.197"/><use xlink:href="#20" y="17.368"/><use xlink:href="#21" y="19.539"/><use xlink:href="#22" y="21.71"/><use xlink:href="#23" y="23.881"/><use xlink:href="#24" y="26.052"/><use xlink:href="#25" y="28.223"/><use xlink:href="#26" y="30.394"/><use xlink:href="#27" y="32.565"/><use xlink:href="#28" y="34.736"/><use xlink:href="#29" y="39.078"/><use xlink:href="#30" y="43.42"/><use xlink:href="#31" y="45.591"/><use xlink:href="#32" y="47.762"/><use xlink:href="#33" y="49.933"/></svg><svg x="2080"><use xlink:href="#a"/><use xlink:href="#13"/><use xlink:href="#14" y="2.171"/><use xlink:href="#15" y="4.342"/><use xlink:href="#16" y="6.513"/><use xlink:href="#17" y="8.684"/><use xlink:href="#18" y="10.855"/><use xlink:href="#19" y="13.026"/><use xlink:href="#20" y="15.197"/><use xlink:href="#21" y="17.368"/><use xlink:href="#22" y="19.539"/><use xlink:href="#23" y="21.71"/><use xlink:href="#24" y="23.881"/><use xlink:href="#25" y="26.052"/><use xlink:href="#26" y="28.223"/><use xlink:href="#27" y="30.394"/><use xlink:href="#28" y="32.565"/><use xlink:href="#29" y="36.907"/><use xlink:href="#30" y="41.249"/><use xlink:href="#31" y="43.42"/><use xlink:href="#32" y="45.591"/><use xlink:href="#33" y="47.762"/><use xlink:href="#34" y="49.933"/></svg><svg x="2160"><use xlink:href="#a"/><use xlink:href="#14"/><use xlink:href="#15" y="2.171"/><use xlink:href="#16" y="4.342"/><use xlink:href="#17" y="6.513"/><use xlink:href="#18" y="8.684"/><use xlink:href="#19" y="10.855"/><use xlink:href="#20" y="13.026"/><use xlink:href="#21" y="15.197"/><use xlink:href="#22" y="17.368"/><use xlink:href="#23" y="19.539"/><use xlink:href="#24" y="21.71"/><use xlink:href="#25" y="23.881"/><use xlink:href="#26" y="26.052"/><use xlink:href="#27" y="28.223"/><use xlink:href="#28" y="30.394"/><use xlink:href="#29" y="34.736"/><use xlink:href="#30" y="39.078"/><use xlink:href="#31" y="41.249"/><use xlink:href="#32" y="43.42"/><use xlink:href="#33" y="45.591"/><use xlink:href="#34" y="47.762"/><use xlink:href="#35" y="49.933"/></svg><svg x="2240"><use xlink:href="#a"/><use xlink:href="#15"/><use xlink:href="#16" y="2.171"/><use xlink:href="#17" y="4.342"/><use xlink:href="#18" y="6.513"/><use xlink:href="#19" y="8.684"/><use xlink:href="#20" y="10.855"/><use xlink:href="#21" y="13.026"/><use xlink:href="#22" y="15.197"/><use xlink:href="#23" y="17.368"/><use xlink:href="#24" y="19.539"/><use xlink:href="#25" y="21.71"/><use xlink:href="#26" y="23.881"/><use xlink:href="#27" y="26.052"/><use xlink:href="#28" y="28.223"/><use xlink:href="#29" y="32.565"/><use xlink:href="#30" y="36.907"/><use xlink:href="#31" y="39.078"/><use xlink:href="#32" y="41.249"/><use xlink:href="#33" y="43.42"/><use xlink:href="#34" y="45.591"/><use xlink:href="#35" y="47.762"/><use xlink:href="#36" y="49.933"/></svg><svg x="2320"><use xlink:href="#a"/><use xlink:href="#16"/><use xlink:href="#17" y="2.171"/><use xlink:href="#18" y="4.342"/><use xlink:href="#19" y="6.513"/><use xlink:href="#20" y="8.684"/><use xlink:href="#21" y="10.855"/><use xlink:href="#22" y="13.026"/><use xlink:href="#23" y="15.197"/><use xlink:href="#24" y="17.368"/><use xlink:href="#25" y="19.539"/><use xlink:href="#26" y="21.71"/><use xlink:href="#27" y="23.881"/><use xlink:href="#28" y="26.052"/><use xlink:href="#29" y="30.394"/><use xlink:href="#30" y="34.736"/><use xlink:href="#31" y="36.907"/><use xlink:href="#32" y="39.078"/><use xlink:href="#33" y="41.249"/><use xlink:href="#34" y="43.42"/><use xlink:href="#35" y="45.591"/><use xlink:href="#36" y="47.762"/><use xlink:href="#37" y="49.933"/></svg><svg x="2400"><use xlink:href="#a"/><use xlink:href="#17"/><use xlink:href="#18" y="2.171"/><use xlink:href="#19" y="4.342"/><use xlink:href="#20" y="6.513"/><use xlink:href="#21" y="8.684"/><use xlink:href="#22" y="10.855"/><use xlink:href="#23" y="13.026"/><use xlink:href="#24" y="15.197"/><use xlink:href="#25" y="17.368"/><use xlink:href="#26" y="19.539"/><use xlink:href="#27" y="21.71"/><use xlink:href="#28" y="23.881"/><use xlink:href="#29" y="28.223"/><use xlink:href="#30" y="32.565"/><use xlink:href="#31" y="34.736"/><use xlink:href="#32" y="36.907"/><use xlink:href="#33" y="39.078"/><use xlink:href="#34" y="41.249"/><use xlink:href="#35" y="43.42"/><use xlink:href="#36" y="45.591"/><use xlink:href="#37" y="47.762"/></svg><svg x="2480"><use xlink:href="#a"/><use xlink:href="#19"/><use xlink:href="#20" y="2.171"/><use xlink:href="#21" y="4.342"/><use xlink:href="#22" y="6.513"/><use xlink:href="#23" y="8.684"/><use xlink:href="#24" y="10.855"/><use xlink:href="#25" y="13.026"/><use xlink:href="#26" y="15.197"/><use xlink:href="#27" y="17.368"/><use xlink:href="#28" y="19.539"/><use xlink:href="#29" y="23.881"/><use xlink:href="#30" y="28.223"/><use xlink:href="#31" y="30.394"/><use xlink:href="#32" y="32.565"/><use xlink:href="#33" y="34.736"/><use xlink:href="#34" y="36.907"/><use xlink:href="#35" y="39.078"/><use xlink:href="#36" y="41.249"/><use xlink:href="#37" y="43.42"/><use xlink:href="#38" y="49.933"/></svg><svg x="2560"><use xlink:href="#a"/><use xlink:href="#22"/><use xlink:href="#23" y="2.171"/><use xlink:href="#24" y="4.342"/><use xlink:href="#25" y="6.513"/><use xlink:href="#26" y="8.684"/><use xlink:href="#27" y="10.855"/><use xlink:href="#28" y="13.026"/><use xlink:href="#29" y="17.368"/><use xlink:href="#30" y="21.71"/><use xlink:href="#31" y="23.881"/><use xlink:href="#32" y="26.052"/><use xlink:href="#33" y="28.223"/><use xlink:href="#34" y="30.394"/><use xlink:href="#35" y="32.565"/><use xlink:href="#36" y="34.736"/><use xlink:href="#37" y="36.907"/><use xlink:href="#38" y="43.42"/><use xlink:href="#39" y="45.591"/><use xlink:href="#40" y="49.933"/></svg><svg x="2640"><use xlink:href="#a"/><use xlink:href="#24"/><use xlink:href="#25" y="2.171"/><use xlink:href="#26" y="4.342"/><use xlink:href="#27" y="6.513"/><use xlink:href="#28" y="8.684"/><use xlink:href="#29" y="13.026"/><use xlink:href="#30" y="17.368"/><use xlink:href="#31" y="19.539"/><use xlink:href="#32" y="21.71"/><use xlink:href="#33" y="23.881"/><use xlink:href="#34" y="26.052"/><use xlink:href="#35" y="28.223"/><use xlink:href="#36" y="30.394"/><use xlink:href="#37" y="32.565"/><use xlink:href="#38" y="39.078"/><use xlink:href="#39" y="41.249"/><use xlink:href="#40" y="45.591"/><use xlink:href="#41" y="49.933"/></svg><svg x="2720"><use xlink:href="#a"/><use xlink:href="#25"/><use xlink:href="#26" y="2.171"/><use xlink:href="#27" y="4.342"/><use xlink:href="#28" y="6.513"/><use xlink:href="#29" y="10.855"/><use xlink:href="#30" y="15.197"/><use xlink:href="#31" y="17.368"/><use xlink:href="#32" y="19.539"/><use xlink:href="#33" y="21.71"/><use xlink:href="#34" y="23.881"/><use xlink:href="#35" y="26.052"/><use xlink:href="#36" y="28.223"/><use xlink:href="#37" y="30.394"/><use xlink:href="#38" y="36.907"/><use xlink:href="#39" y="39.078"/><use xlink:href="#40" y="43.42"/><use xlink:href="#41" y="47.762"/><use xlink:href="#42" y="49.933"/></svg><svg x="2800"><use xlink:href="#a"/><use xlink:href="#27"/><use xlink:href="#28" y="2.171"/><use xlink:href="#29" y="6.513"/><use xlink:href="#30" y="10.855"/><use xlink:href="#31" y="13.026"/><use xlink:href="#32" y="15.197"/><use xlink:href="#33" y="17.368"/><use xlink:href="#34" y="19.539"/><use xlink:href="#35" y="21.71"/><use xlink:href="#36" y="23.881"/><use xlink:href="#37" y="26.052"/><use xlink:href="#38" y="32.565"/><use xlink:href="#39" y="34.736"/><use xlink:href="#40" y="39.078"/><use xlink:href="#41" y="43.42"/><use xlink:href="#42" y="45.591"/><use xlink:href="#43" y="49.933"/></svg><svg x="2880"><use xlink:href="#a"/><use xlink:href="#28"/><use xlink:href="#29" y="4.342"/><use xlink:href="#30" y="8.684"/><use xlink:href="#31" y="10.855"/><use xlink:href="#32" y="13.026"/><use xlink:href="#33" y="15.197"/><use xlink:href="#34" y="17.368"/><use xlink:href="#35" y="19.539"/><use xlink:href="#36" y="21.71"/><use xlink:href="#37" y="23.881"/><use xlink:href="#38" y="30.394"/><use xlink:href="#39" y="32.565"/><use xlink:href="#40" y="36.907"/><use xlink:href="#41" y="41.249"/><use xlink:href="#42" y="43.42"/><use xlink:href="#43" y="47.762"/><text y="51.603" class="f">Exit</text><text x="5.01" y="51.603" class="f">code:</text><text x="11.022" y="51.603" class="f">0</text><text x="13.026" y="51.603" class="f">(allowed)</text></svg></svg></g></g></svg></svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-safe-setup",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "One command to make Claude Code safe for autonomous operation. 7 hooks: destructive blocker, branch guard, force-push protection, secret leak prevention, syntax checks, and more.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"bin": {
|
|
@@ -8,12 +8,25 @@
|
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
10
|
"claude-code",
|
|
11
|
+
"claude",
|
|
12
|
+
"anthropic",
|
|
11
13
|
"ai",
|
|
12
14
|
"safety",
|
|
13
15
|
"hooks",
|
|
14
16
|
"autonomous",
|
|
15
|
-
"cli"
|
|
17
|
+
"cli",
|
|
18
|
+
"pretooluse",
|
|
19
|
+
"guard",
|
|
20
|
+
"rm-rf",
|
|
21
|
+
"git-push",
|
|
22
|
+
"env",
|
|
23
|
+
"secrets",
|
|
24
|
+
"syntax-check",
|
|
25
|
+
"context-window"
|
|
16
26
|
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "bash test.sh"
|
|
29
|
+
},
|
|
17
30
|
"author": "yurukusa",
|
|
18
31
|
"license": "MIT",
|
|
19
32
|
"repository": {
|
package/scripts.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"destructive-guard": "#!/bin/bash\n# ================================================================\n# destructive-guard.sh — Destructive Command Blocker\n# ================================================================\n# PURPOSE:\n# Blocks dangerous shell commands that can cause irreversible damage.\n# Catches rm -rf on sensitive paths, git reset --hard, git clean -fd,\n# and other destructive operations before they execute.\n#\n# Built after a real incident where rm -rf on a pnpm project\n# followed NTFS junctions and deleted an entire C:\\Users directory.\n# (GitHub Issue #36339)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - rm -rf / rm -r on root, home, or parent paths (/, ~, .., /home, /etc)\n# - git reset --hard\n# - git clean -fd / git clean -fdx\n# - chmod -R 777 on sensitive paths\n# - find ... -delete on broad patterns\n#\n# WHAT IT ALLOWS (exit 0):\n# - rm -rf on specific project subdirectories (node_modules, dist, build)\n# - git reset --soft, git reset HEAD\n# - All non-destructive commands\n#\n# CONFIGURATION:\n# CC_ALLOW_DESTRUCTIVE=1 — disable this guard (not recommended)\n# CC_SAFE_DELETE_DIRS — colon-separated list of safe-to-delete dirs\n# default: \"node_modules:dist:build:.cache:__pycache__:coverage\"\n#\n# NOTE: On Windows/WSL2, rm -rf can follow NTFS junctions (symlinks)\n# and delete far more than intended. This guard is especially critical\n# on WSL2 environments.\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty')\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Allow override (not recommended)\nif [[ \"${CC_ALLOW_DESTRUCTIVE:-0}\" == \"1\" ]]; then\n exit 0\nfi\n\n# Log function — records blocked commands for audit\nlog_block() {\n local reason=\"$1\"\n local logfile=\"${CC_BLOCK_LOG:-$HOME/.claude/blocked-commands.log}\"\n mkdir -p \"$(dirname \"$logfile\")\" 2>/dev/null\n echo \"[$(date -Iseconds)] BLOCKED: $reason | cmd: $COMMAND\" >> \"$logfile\" 2>/dev/null\n}\n\n# Safe directories that can be deleted\nSAFE_DIRS=\"${CC_SAFE_DELETE_DIRS:-node_modules:dist:build:.cache:__pycache__:coverage:.next:.nuxt:tmp}\"\n\n# --- Check 1: rm -rf on dangerous paths ---\nif echo \"$COMMAND\" | grep -qE 'rm\\s+(-[rf]+\\s+)*(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|\\/var|~\\/|~\\s*$|\\.\\.\\/|\\.\\.\\s*$)'; then\n # Exception: safe directories\n SAFE=0\n IFS=':' read -ra DIRS <<< \"$SAFE_DIRS\"\n for dir in \"${DIRS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"rm\\s+.*${dir}\\s*$|rm\\s+.*${dir}/\"; then\n SAFE=1\n break\n fi\n done\n\n # Check for mounted filesystems inside the target (NFS, Docker, bind mounts)\n # Why: GitHub #36640 — rm -rf on a dir with NFS mount deleted production data\n if (( SAFE == 0 )); then\n # Extract the target path from the rm command\n TARGET_PATH=$(echo \"$COMMAND\" | grep -oP 'rm\\s+(-[rf]+\\s+)*\\K\\S+')\n if [ -n \"$TARGET_PATH\" ] && command -v findmnt &>/dev/null; then\n if findmnt -n -o TARGET --submounts \"$TARGET_PATH\" 2>/dev/null | grep -q .; then\n log_block \"rm on path with mounted filesystem\"\n echo \"BLOCKED: Target contains a mounted filesystem (NFS, Docker, bind).\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Unmount the filesystem first, then retry.\" >&2\n exit 2\n fi\n fi\n fi\n\n if (( SAFE == 0 )); then\n log_block \"rm on sensitive path\"\n echo \"BLOCKED: rm on sensitive path detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"This command targets a sensitive directory that could cause\" >&2\n echo \"irreversible data loss. On WSL2, rm -rf can follow NTFS\" >&2\n echo \"junctions and delete far beyond the target directory.\" >&2\n echo \"\" >&2\n echo \"If you need to delete a specific subdirectory, target it directly:\" >&2\n echo \" rm -rf ./specific-folder\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: git reset --hard ---\n# Only match when git is the actual command, not inside strings/arguments\nif echo \"$COMMAND\" | grep -qE '^\\s*git\\s+reset\\s+--hard|;\\s*git\\s+reset\\s+--hard|&&\\s*git\\s+reset\\s+--hard|\\|\\|\\s*git\\s+reset\\s+--hard'; then\n log_block \"git reset --hard\"\n echo \"BLOCKED: git reset --hard discards all uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash, or git reset --soft to keep changes staged.\" >&2\n exit 2\nfi\n\n# --- Check 3: git clean -fd ---\nif echo \"$COMMAND\" | grep -qE '^\\s*git\\s+clean\\s+-[a-z]*[fd]|;\\s*git\\s+clean|&&\\s*git\\s+clean|\\|\\|\\s*git\\s+clean'; then\n log_block \"git clean\"\n echo \"BLOCKED: git clean removes untracked files permanently.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git clean -n (dry run) first to see what would be deleted.\" >&2\n exit 2\nfi\n\n# --- Check 4: chmod 777 on broad paths ---\nif echo \"$COMMAND\" | grep -qE 'chmod\\s+(-R\\s+)?777\\s+(\\/|~|\\.)'; then\n echo \"BLOCKED: chmod 777 on broad path is a security risk.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n exit 2\nfi\n\n# --- Check 5: find -delete on broad patterns ---\nif echo \"$COMMAND\" | grep -qE 'find\\s+(\\/|~|\\.\\.)\\s.*-delete'; then\n echo \"BLOCKED: find -delete on broad path risks mass deletion.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: find ... -print first to verify what matches.\" >&2\n exit 2\nfi\n\nexit 0\n",
|
|
2
|
+
"destructive-guard": "#!/bin/bash\n# ================================================================\n# destructive-guard.sh — Destructive Command Blocker\n# ================================================================\n# PURPOSE:\n# Blocks dangerous shell commands that can cause irreversible damage.\n# Catches rm -rf on sensitive paths, git reset --hard, git clean -fd,\n# and other destructive operations before they execute.\n#\n# Built after a real incident where rm -rf on a pnpm project\n# followed NTFS junctions and deleted an entire C:\\Users directory.\n# (GitHub Issue #36339)\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - rm -rf / rm -r on root, home, or parent paths (/, ~, .., /home, /etc)\n# - git reset --hard\n# - git clean -fd / git clean -fdx\n# - chmod -R 777 on sensitive paths\n# - find ... -delete on broad patterns\n#\n# WHAT IT ALLOWS (exit 0):\n# - rm -rf on specific project subdirectories (node_modules, dist, build)\n# - git reset --soft, git reset HEAD\n# - All non-destructive commands\n#\n# CONFIGURATION:\n# CC_ALLOW_DESTRUCTIVE=1 — disable this guard (not recommended)\n# CC_SAFE_DELETE_DIRS — colon-separated list of safe-to-delete dirs\n# default: \"node_modules:dist:build:.cache:__pycache__:coverage\"\n#\n# NOTE: On Windows/WSL2, rm -rf can follow NTFS junctions (symlinks)\n# and delete far more than intended. This guard is especially critical\n# on WSL2 environments.\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty')\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Allow override (not recommended)\nif [[ \"${CC_ALLOW_DESTRUCTIVE:-0}\" == \"1\" ]]; then\n exit 0\nfi\n\n# Log function — records blocked commands for audit\nlog_block() {\n local reason=\"$1\"\n local logfile=\"${CC_BLOCK_LOG:-$HOME/.claude/blocked-commands.log}\"\n mkdir -p \"$(dirname \"$logfile\")\" 2>/dev/null\n echo \"[$(date -Iseconds)] BLOCKED: $reason | cmd: $COMMAND\" >> \"$logfile\" 2>/dev/null\n}\n\n# Safe directories that can be deleted\nSAFE_DIRS=\"${CC_SAFE_DELETE_DIRS:-node_modules:dist:build:.cache:__pycache__:coverage:.next:.nuxt:tmp}\"\n\n# --- Check 1: rm -rf on dangerous paths ---\nif echo \"$COMMAND\" | grep -qE 'rm\\s+(-[rf]+\\s+)*(\\/$|\\/\\s|\\/[^a-z]|\\/home|\\/etc|\\/usr|\\/var|~\\/|~\\s*$|\\.\\.\\/|\\.\\.\\s*$)'; then\n # Exception: safe directories\n SAFE=0\n IFS=':' read -ra DIRS <<< \"$SAFE_DIRS\"\n for dir in \"${DIRS[@]}\"; do\n if echo \"$COMMAND\" | grep -qE \"rm\\s+.*${dir}\\s*$|rm\\s+.*${dir}/\"; then\n SAFE=1\n break\n fi\n done\n\n # Check for mounted filesystems inside the target (NFS, Docker, bind mounts)\n # Why: GitHub #36640 — rm -rf on a dir with NFS mount deleted production data\n if (( SAFE == 0 )); then\n # Extract the target path from the rm command\n TARGET_PATH=$(echo \"$COMMAND\" | grep -oP 'rm\\s+(-[rf]+\\s+)*\\K\\S+')\n if [ -n \"$TARGET_PATH\" ] && command -v findmnt &>/dev/null; then\n if findmnt -n -o TARGET --submounts \"$TARGET_PATH\" 2>/dev/null | grep -q .; then\n log_block \"rm on path with mounted filesystem\"\n echo \"BLOCKED: Target contains a mounted filesystem (NFS, Docker, bind).\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Unmount the filesystem first, then retry.\" >&2\n exit 2\n fi\n fi\n fi\n\n if (( SAFE == 0 )); then\n log_block \"rm on sensitive path\"\n echo \"BLOCKED: rm on sensitive path detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"This command targets a sensitive directory that could cause\" >&2\n echo \"irreversible data loss. On WSL2, rm -rf can follow NTFS\" >&2\n echo \"junctions and delete far beyond the target directory.\" >&2\n echo \"\" >&2\n echo \"If you need to delete a specific subdirectory, target it directly:\" >&2\n echo \" rm -rf ./specific-folder\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: git reset --hard ---\n# Only match when git is the actual command, not inside strings/arguments\nif echo \"$COMMAND\" | grep -qE '^\\s*git\\s+reset\\s+--hard|;\\s*git\\s+reset\\s+--hard|&&\\s*git\\s+reset\\s+--hard|\\|\\|\\s*git\\s+reset\\s+--hard'; then\n log_block \"git reset --hard\"\n echo \"BLOCKED: git reset --hard discards all uncommitted changes.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git stash, or git reset --soft to keep changes staged.\" >&2\n exit 2\nfi\n\n# --- Check 3: git clean -fd ---\nif echo \"$COMMAND\" | grep -qE '^\\s*git\\s+clean\\s+-[a-z]*[fd]|;\\s*git\\s+clean|&&\\s*git\\s+clean|\\|\\|\\s*git\\s+clean'; then\n log_block \"git clean\"\n echo \"BLOCKED: git clean removes untracked files permanently.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: git clean -n (dry run) first to see what would be deleted.\" >&2\n exit 2\nfi\n\n# --- Check 4: chmod 777 on broad paths ---\nif echo \"$COMMAND\" | grep -qE 'chmod\\s+(-R\\s+)?777\\s+(\\/|~|\\.)'; then\n echo \"BLOCKED: chmod 777 on broad path is a security risk.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n exit 2\nfi\n\n# --- Check 5: find -delete on broad patterns ---\nif echo \"$COMMAND\" | grep -qE 'find\\s+(\\/|~|\\.\\.)\\s.*-delete'; then\n echo \"BLOCKED: find -delete on broad path risks mass deletion.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Consider: find ... -print first to verify what matches.\" >&2\n exit 2\nfi\n\nexit 0\n# --- Check 6: sudo with dangerous commands ---\nif echo \"$COMMAND\" | grep -qE '^\\s*sudo\\s+(rm\\s+-[rf]|chmod\\s+(-R\\s+)?777|dd\\s+if=)'; then\n log_block \"sudo with dangerous command\"\n echo \"BLOCKED: sudo with dangerous command detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Running destructive commands with sudo amplifies the damage.\" >&2\n echo \"Review the command carefully before proceeding.\" >&2\n exit 2\nfi\n\nexit 0\n",
|
|
3
3
|
"branch-guard": "#!/bin/bash\n# ================================================================\n# branch-guard.sh — Branch Push Protector\n# ================================================================\n# PURPOSE:\n# Prevents accidental git push to main/master branches AND\n# blocks force-push on ALL branches without explicit approval.\n#\n# Force-pushes rewrite history and can destroy teammates' work.\n# Protected branch pushes bypass code review.\n#\n# TRIGGER: PreToolUse\n# MATCHER: \"Bash\"\n#\n# WHAT IT BLOCKS (exit 2):\n# - git push origin main/master (any protected branch)\n# - git push --force (any branch — history rewriting)\n# - git push -f (short flag variant)\n# - git push --force-with-lease (still destructive)\n#\n# WHAT IT ALLOWS (exit 0):\n# - git push origin feature-branch (non-force)\n# - git push -u origin feature-branch\n# - All other git commands\n# - All non-git commands\n#\n# CONFIGURATION:\n# CC_PROTECT_BRANCHES — colon-separated list of protected branches\n# default: \"main:master\"\n# CC_ALLOW_FORCE_PUSH=1 — disable force-push protection\n# ================================================================\n\nINPUT=$(cat)\nCOMMAND=$(echo \"$INPUT\" | jq -r '.tool_input.command // empty')\n\nif [[ -z \"$COMMAND\" ]]; then\n exit 0\nfi\n\n# Only check git push commands\nif ! echo \"$COMMAND\" | grep -qE '^\\s*git\\s+push'; then\n exit 0\nfi\n\n# --- Check 1: Force push on ANY branch ---\nif [[ \"${CC_ALLOW_FORCE_PUSH:-0}\" != \"1\" ]]; then\n if echo \"$COMMAND\" | grep -qE 'git\\s+push\\s+.*(-f\\b|--force\\b|--force-with-lease\\b)'; then\n echo \"BLOCKED: Force push detected.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Force push rewrites remote history and can destroy\" >&2\n echo \"other people's work. This is almost never what you want.\" >&2\n echo \"\" >&2\n echo \"If you truly need to force push, set CC_ALLOW_FORCE_PUSH=1\" >&2\n exit 2\n fi\nfi\n\n# --- Check 2: Push to protected branches ---\nPROTECTED=\"${CC_PROTECT_BRANCHES:-main:master}\"\n\nBLOCKED=0\nIFS=':' read -ra BRANCHES <<< \"$PROTECTED\"\nfor branch in \"${BRANCHES[@]}\"; do\n if echo \"$COMMAND\" | grep -qwE \"origin\\s+${branch}|${branch}\\s|${branch}$\"; then\n BLOCKED=1\n break\n fi\ndone\n\nif (( BLOCKED == 1 )); then\n echo \"BLOCKED: Attempted push to protected branch.\" >&2\n echo \"\" >&2\n echo \"Command: $COMMAND\" >&2\n echo \"\" >&2\n echo \"Protected branches: $PROTECTED\" >&2\n echo \"\" >&2\n echo \"Push to a feature branch first, then create a pull request.\" >&2\n exit 2\nfi\n\nexit 0\n",
|
|
4
4
|
"syntax-check": "#!/bin/bash\n# ================================================================\n# syntax-check.sh — Automatic Syntax Validation After Edits\n# ================================================================\n# PURPOSE:\n# Runs syntax checks immediately after Claude Code edits or\n# writes a file. Catches syntax errors before they propagate\n# into downstream failures.\n#\n# SUPPORTED LANGUAGES:\n# .py — python -m py_compile\n# .sh — bash -n\n# .bash — bash -n\n# .json — jq empty\n# .yaml — python3 yaml.safe_load (if PyYAML installed)\n# .yml — python3 yaml.safe_load (if PyYAML installed)\n# .js — node --check (if node installed)\n# .ts — npx tsc --noEmit (if tsc available) [EXPERIMENTAL]\n#\n# TRIGGER: PostToolUse\n# MATCHER: \"Edit|Write\"\n#\n# DESIGN PHILOSOPHY:\n# - Never blocks (always exit 0) — reports errors but doesn't\n# prevent the edit from completing\n# - Silent on success — only speaks up when something is wrong\n# - Fails open — if a checker isn't installed, silently skips\n#\n# BORN FROM:\n# Countless sessions where Claude Code introduced a syntax error,\n# continued working for 10+ tool calls, then hit a wall when\n# trying to run the broken file. Catching it immediately saves\n# context window and frustration.\n# ================================================================\n\nINPUT=$(cat)\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty')\n\n# No file path = nothing to check\nif [[ -z \"$FILE_PATH\" || ! -f \"$FILE_PATH\" ]]; then\n exit 0\nfi\n\nEXT=\"${FILE_PATH##*.}\"\n\ncase \"$EXT\" in\n py)\n if python3 -m py_compile \"$FILE_PATH\" 2>&1; then\n : # silent on success\n else\n echo \"SYNTAX ERROR (Python): $FILE_PATH\" >&2\n fi\n ;;\n sh|bash)\n if bash -n \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (Shell): $FILE_PATH\" >&2\n fi\n ;;\n json)\n if command -v jq &>/dev/null; then\n if jq empty \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (JSON): $FILE_PATH\" >&2\n fi\n fi\n ;;\n yaml|yml)\n if python3 -c \"import yaml\" 2>/dev/null; then\n if python3 -c \"\nimport yaml, sys\nwith open(sys.argv[1]) as f:\n yaml.safe_load(f)\n\" \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (YAML): $FILE_PATH\" >&2\n fi\n fi\n ;;\n js)\n if command -v node &>/dev/null; then\n if node --check \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (JavaScript): $FILE_PATH\" >&2\n fi\n fi\n ;;\n ts)\n # EXPERIMENTAL: TypeScript check requires tsc in PATH\n if command -v npx &>/dev/null; then\n if npx tsc --noEmit \"$FILE_PATH\" 2>&1; then\n :\n else\n echo \"SYNTAX ERROR (TypeScript) [experimental]: $FILE_PATH\" >&2\n fi\n fi\n ;;\n *)\n # Unknown extension — skip silently\n ;;\nesac\n\nexit 0\n",
|
|
5
5
|
"context-monitor": "#!/bin/bash\n# ================================================================\n# context-monitor.sh — Context Window Remaining Capacity Monitor\n# ================================================================\n# PURPOSE:\n# Monitors how much context window remains during a Claude Code\n# session. Issues graduated warnings (CAUTION → WARNING → CRITICAL\n# → EMERGENCY) so you never get killed by context exhaustion.\n#\n# HOW IT WORKS:\n# 1. Reads Claude Code's debug log to extract actual token usage\n# 2. Falls back to tool-call-count estimation when debug logs\n# are unavailable\n# 3. Saves current % to /tmp/cc-context-pct (other scripts can\n# read this)\n# 4. At CRITICAL/EMERGENCY, writes an evacuation template to\n# your mission file so you can hand off state before /compact\n#\n# TRIGGER: PostToolUse (all tools)\n# MATCHER: \"\" (empty = every tool invocation)\n#\n# CONFIGURATION:\n# CC_CONTEXT_MISSION_FILE — path to your mission/state file\n# default: $HOME/mission.md\n#\n# THRESHOLDS (edit below to taste):\n# CAUTION = 40% — be mindful of consumption\n# WARNING = 25% — finish current task, save state\n# CRITICAL = 20% — run /compact immediately\n# EMERGENCY = 15% — stop everything, evacuate\n#\n# BORN FROM:\n# A session that hit 3% context remaining with no warning.\n# The agent died mid-task and all in-flight work was lost.\n# Never again.\n# ================================================================\n\nSTATE_FILE=\"/tmp/cc-context-state\"\nPCT_FILE=\"/tmp/cc-context-pct\"\nCOUNTER_FILE=\"/tmp/cc-context-monitor-count\"\nMISSION_FILE=\"${CC_CONTEXT_MISSION_FILE:-$HOME/mission.md}\"\n\n# Tool invocation counter (fallback estimator)\nCOUNT=$(cat \"$COUNTER_FILE\" 2>/dev/null || echo 0)\nCOUNT=$((COUNT + 1))\necho \"$COUNT\" > \"$COUNTER_FILE\"\n\n# Check every 3rd invocation to reduce overhead\n# (but always check in CRITICAL/EMERGENCY state)\nLAST_STATE=$(cat \"$STATE_FILE\" 2>/dev/null || echo \"normal\")\nif [ $((COUNT % 3)) -ne 0 ] && [ \"$LAST_STATE\" != \"critical\" ] && [ \"$LAST_STATE\" != \"emergency\" ]; then\n exit 0\nfi\n\n# --- Extract context % from Claude Code debug logs ---\nget_context_pct() {\n local debug_dir=\"$HOME/.claude/debug\"\n if [ ! -d \"$debug_dir\" ]; then\n echo \"\"\n return\n fi\n\n local latest\n latest=$(find \"$debug_dir\" -maxdepth 1 -name '*.txt' -printf '%T@ %p\\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2)\n if [ -z \"$latest\" ]; then\n echo \"\"\n return\n fi\n\n # Parse the last autocompact entry for token counts\n local line\n line=$(grep 'autocompact:' \"$latest\" 2>/dev/null | tail -1)\n if [ -z \"$line\" ]; then\n echo \"\"\n return\n fi\n\n local tokens window\n tokens=$(echo \"$line\" | sed 's/.*tokens=\\([0-9]*\\).*/\\1/')\n window=$(echo \"$line\" | sed 's/.*effectiveWindow=\\([0-9]*\\).*/\\1/')\n\n if [ -n \"$tokens\" ] && [ -n \"$window\" ] && [ \"$window\" -gt 0 ] 2>/dev/null; then\n local pct\n pct=$(( (window - tokens) * 100 / window ))\n echo \"$pct\"\n else\n echo \"\"\n fi\n}\n\nCONTEXT_PCT=$(get_context_pct)\n\n# Fallback: estimate from tool call count when debug logs unavailable\n# Assumes ~180 tool calls fills ~100% of context (conservative)\nif [ -z \"$CONTEXT_PCT\" ]; then\n CONTEXT_PCT=$(( 100 - (COUNT * 100 / 180) ))\n if [ \"$CONTEXT_PCT\" -lt 0 ]; then CONTEXT_PCT=0; fi\n SOURCE=\"estimate\"\nelse\n SOURCE=\"debug\"\nfi\n\necho \"$CONTEXT_PCT\" > \"$PCT_FILE\"\n\nTIMESTAMP=$(date '+%Y-%m-%d %H:%M')\n\n# --- Evacuation template (with cooldown to prevent spam) ---\nEVAC_COOLDOWN_FILE=\"/tmp/cc-context-evac-last\"\nEVAC_COOLDOWN_SEC=1800 # 30 min cooldown between template generations\n\ngenerate_evacuation_template() {\n local level=\"$1\"\n\n # Cooldown check\n if [ -f \"$EVAC_COOLDOWN_FILE\" ]; then\n local last_ts now_ts diff\n last_ts=$(cat \"$EVAC_COOLDOWN_FILE\" 2>/dev/null || echo 0)\n now_ts=$(date +%s)\n diff=$((now_ts - last_ts))\n if [ \"$diff\" -lt \"$EVAC_COOLDOWN_SEC\" ]; then\n return\n fi\n fi\n\n # Don't add a new template if there's already an unfilled one\n if [ -f \"$MISSION_FILE\" ] && grep -q '\\[TODO\\]' \"$MISSION_FILE\" 2>/dev/null; then\n return\n fi\n\n date +%s > \"$EVAC_COOLDOWN_FILE\"\n\n # Create mission file directory if needed\n mkdir -p \"$(dirname \"$MISSION_FILE\")\"\n\n cat >> \"$MISSION_FILE\" << EVAC_EOF\n\n## Context Evacuation Template (${level} - ${TIMESTAMP})\n<!-- Auto-generated by context-monitor.sh. Fill in before /compact -->\n### Current Task\n- Task: [TODO]\n- Progress: [TODO]\n- Files being edited: [TODO]\n\n### Git State\n- Branch: [TODO]\n- Uncommitted changes: [TODO]\n\n### Next Action\n- Next command/action: [TODO]\nEVAC_EOF\n}\n\n# --- Graduated warnings ---\nif [ \"$CONTEXT_PCT\" -le 15 ]; then\n # EMERGENCY\n if [ \"$LAST_STATE\" != \"emergency\" ]; then\n echo \"emergency\" > \"$STATE_FILE\"\n generate_evacuation_template \"EMERGENCY\"\n fi\n echo \"\"\n echo \"EMERGENCY: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Fill in the [TODO] fields in the template\"\n echo \"2. Run /compact\"\n echo \"3. If needed, restart and resume from mission file\"\n echo \"No further work allowed. Evacuate only.\"\n\nelif [ \"$CONTEXT_PCT\" -le 20 ]; then\n # CRITICAL\n if [ \"$LAST_STATE\" != \"critical\" ]; then\n echo \"critical\" > \"$STATE_FILE\"\n generate_evacuation_template \"CRITICAL\"\n fi\n echo \"\"\n echo \"CRITICAL: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Run /compact IMMEDIATELY. Evacuation template written to ${MISSION_FILE}.\"\n echo \"1. Save current task state to the template\"\n echo \"2. Run /compact\"\n\nelif [ \"$CONTEXT_PCT\" -le 25 ]; then\n # WARNING\n if [ \"$LAST_STATE\" != \"warning\" ]; then\n echo \"warning\" > \"$STATE_FILE\"\n echo \"\"\n echo \"WARNING: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Do not start new large tasks. Finish current work and save state.\"\n fi\n\nelif [ \"$CONTEXT_PCT\" -le 40 ]; then\n # CAUTION\n if [ \"$LAST_STATE\" != \"caution\" ]; then\n echo \"caution\" > \"$STATE_FILE\"\n echo \"\"\n echo \"CAUTION: Context remaining ${CONTEXT_PCT}% (${SOURCE})\"\n echo \"Be mindful of context consumption. Keep interactions concise.\"\n fi\nfi\n\nexit 0\n",
|
package/test.sh
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# cc-safe-setup hook tests
|
|
3
|
+
# Run: bash test.sh
|
|
4
|
+
# All hooks are tested by piping JSON input and checking exit codes
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
PASS=0
|
|
9
|
+
FAIL=0
|
|
10
|
+
SCRIPTS_JSON="$(dirname "$0")/scripts.json"
|
|
11
|
+
|
|
12
|
+
# Extract hook scripts from scripts.json
|
|
13
|
+
extract_hook() {
|
|
14
|
+
python3 -c "import json; print(json.load(open('$SCRIPTS_JSON'))['$1'])" > "/tmp/test-$1.sh"
|
|
15
|
+
chmod +x "/tmp/test-$1.sh"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test_hook() {
|
|
19
|
+
local name="$1" input="$2" expected_exit="$3" desc="$4"
|
|
20
|
+
local actual_exit=0
|
|
21
|
+
echo "$input" | bash "/tmp/test-$name.sh" > /dev/null 2>/dev/null || actual_exit=$?
|
|
22
|
+
if [ "$actual_exit" -eq "$expected_exit" ]; then
|
|
23
|
+
echo " PASS: $desc"
|
|
24
|
+
PASS=$((PASS + 1))
|
|
25
|
+
else
|
|
26
|
+
echo " FAIL: $desc (expected exit $expected_exit, got $actual_exit)"
|
|
27
|
+
FAIL=$((FAIL + 1))
|
|
28
|
+
fi
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
echo "cc-safe-setup hook tests"
|
|
32
|
+
echo "========================"
|
|
33
|
+
echo ""
|
|
34
|
+
|
|
35
|
+
# --- destructive-guard ---
|
|
36
|
+
echo "destructive-guard:"
|
|
37
|
+
extract_hook "destructive-guard"
|
|
38
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"ls -la"}}' 0 "safe command passes"
|
|
39
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"rm -rf /"}}' 2 "rm -rf / blocked"
|
|
40
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"rm -rf ~/"}}' 2 "rm -rf ~/ blocked"
|
|
41
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"rm -rf ../"}}' 2 "rm -rf ../ blocked"
|
|
42
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"rm -rf node_modules"}}' 0 "rm -rf node_modules allowed"
|
|
43
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"git reset --hard"}}' 2 "git reset --hard blocked"
|
|
44
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"git reset --soft HEAD~1"}}' 0 "git reset --soft allowed"
|
|
45
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"git clean -fd"}}' 2 "git clean -fd blocked"
|
|
46
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"chmod -R 777 /"}}' 2 "chmod 777 / blocked"
|
|
47
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"find / -delete"}}' 2 "find / -delete blocked"
|
|
48
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"echo git reset --hard"}}' 0 "git reset in echo not blocked"
|
|
49
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"sudo rm -rf /home"}}' 2 "sudo rm -rf blocked"
|
|
50
|
+
test_hook "destructive-guard" '{"tool_input":{"command":"sudo apt install jq"}}' 0 "safe sudo command allowed"
|
|
51
|
+
echo ""
|
|
52
|
+
|
|
53
|
+
# --- branch-guard ---
|
|
54
|
+
echo "branch-guard:"
|
|
55
|
+
extract_hook "branch-guard"
|
|
56
|
+
test_hook "branch-guard" '{"tool_input":{"command":"git push origin feature-branch"}}' 0 "push to feature allowed"
|
|
57
|
+
test_hook "branch-guard" '{"tool_input":{"command":"git push -u origin my-branch"}}' 0 "push -u to branch allowed"
|
|
58
|
+
test_hook "branch-guard" '{"tool_input":{"command":"git push origin main"}}' 2 "push to main blocked"
|
|
59
|
+
test_hook "branch-guard" '{"tool_input":{"command":"git push origin master"}}' 2 "push to master blocked"
|
|
60
|
+
test_hook "branch-guard" '{"tool_input":{"command":"git push --force origin feature"}}' 2 "force push blocked"
|
|
61
|
+
test_hook "branch-guard" '{"tool_input":{"command":"git push -f origin feature"}}' 2 "force push -f blocked"
|
|
62
|
+
test_hook "branch-guard" '{"tool_input":{"command":"git push --force-with-lease origin feature"}}' 2 "force-with-lease blocked"
|
|
63
|
+
test_hook "branch-guard" '{"tool_input":{"command":"git status"}}' 0 "non-push git command passes"
|
|
64
|
+
test_hook "branch-guard" '{"tool_input":{"command":"npm install"}}' 0 "non-git command passes"
|
|
65
|
+
echo ""
|
|
66
|
+
|
|
67
|
+
# --- secret-guard ---
|
|
68
|
+
echo "secret-guard:"
|
|
69
|
+
extract_hook "secret-guard"
|
|
70
|
+
test_hook "secret-guard" '{"tool_input":{"command":"git add src/index.js"}}' 0 "git add normal file allowed"
|
|
71
|
+
test_hook "secret-guard" '{"tool_input":{"command":"git add .env"}}' 2 "git add .env blocked"
|
|
72
|
+
test_hook "secret-guard" '{"tool_input":{"command":"git add .env.local"}}' 2 "git add .env.local blocked"
|
|
73
|
+
test_hook "secret-guard" '{"tool_input":{"command":"git add credentials.json"}}' 2 "git add credentials.json blocked"
|
|
74
|
+
test_hook "secret-guard" '{"tool_input":{"command":"git add id_rsa"}}' 2 "git add id_rsa blocked"
|
|
75
|
+
test_hook "secret-guard" '{"tool_input":{"command":"git add server.key"}}' 2 "git add .key file blocked"
|
|
76
|
+
test_hook "secret-guard" '{"tool_input":{"command":"npm install"}}' 0 "non-git command passes"
|
|
77
|
+
test_hook "secret-guard" '{"tool_input":{"command":"git commit -m test"}}' 0 "git commit passes"
|
|
78
|
+
echo ""
|
|
79
|
+
|
|
80
|
+
# --- comment-strip ---
|
|
81
|
+
echo "comment-strip:"
|
|
82
|
+
extract_hook "comment-strip"
|
|
83
|
+
# comment-strip outputs JSON on stdout when it modifies, exit 0 always
|
|
84
|
+
local_exit=0
|
|
85
|
+
result=$(echo '{"tool_input":{"command":"# check status\ngit status"}}' | bash /tmp/test-comment-strip.sh 2>/dev/null) || local_exit=$?
|
|
86
|
+
if [ "$local_exit" -eq 0 ] && echo "$result" | python3 -c "import json,sys; d=json.load(sys.stdin); assert 'git status' in d['hookSpecificOutput']['updatedInput']['command']" 2>/dev/null; then
|
|
87
|
+
echo " PASS: strips comment, returns clean command"
|
|
88
|
+
PASS=$((PASS + 1))
|
|
89
|
+
else
|
|
90
|
+
echo " FAIL: comment stripping"
|
|
91
|
+
FAIL=$((FAIL + 1))
|
|
92
|
+
fi
|
|
93
|
+
result2=$(echo '{"tool_input":{"command":"git status"}}' | bash /tmp/test-comment-strip.sh 2>/dev/null) || true
|
|
94
|
+
if [ -z "$result2" ]; then
|
|
95
|
+
echo " PASS: no-comment command passes through unchanged"
|
|
96
|
+
PASS=$((PASS + 1))
|
|
97
|
+
else
|
|
98
|
+
echo " FAIL: should pass through without modification"
|
|
99
|
+
FAIL=$((FAIL + 1))
|
|
100
|
+
fi
|
|
101
|
+
echo ""
|
|
102
|
+
|
|
103
|
+
# --- cd-git-allow ---
|
|
104
|
+
echo "cd-git-allow:"
|
|
105
|
+
extract_hook "cd-git-allow"
|
|
106
|
+
local_exit=0
|
|
107
|
+
result=$(echo '{"tool_input":{"command":"cd /tmp && git log"}}' | bash /tmp/test-cd-git-allow.sh 2>/dev/null) || local_exit=$?
|
|
108
|
+
if [ "$local_exit" -eq 0 ] && echo "$result" | grep -q "permissionDecision"; then
|
|
109
|
+
echo " PASS: cd+git log auto-approved"
|
|
110
|
+
PASS=$((PASS + 1))
|
|
111
|
+
else
|
|
112
|
+
echo " FAIL: cd+git log should be auto-approved"
|
|
113
|
+
FAIL=$((FAIL + 1))
|
|
114
|
+
fi
|
|
115
|
+
result2=$(echo '{"tool_input":{"command":"cd /tmp && git push origin main"}}' | bash /tmp/test-cd-git-allow.sh 2>/dev/null) || true
|
|
116
|
+
if ! echo "$result2" | grep -q "permissionDecision" 2>/dev/null; then
|
|
117
|
+
echo " PASS: cd+git push NOT auto-approved"
|
|
118
|
+
PASS=$((PASS + 1))
|
|
119
|
+
else
|
|
120
|
+
echo " FAIL: cd+git push should not be auto-approved"
|
|
121
|
+
FAIL=$((FAIL + 1))
|
|
122
|
+
fi
|
|
123
|
+
test_hook "cd-git-allow" '{"tool_input":{"command":"npm install"}}' 0 "non-cd command passes"
|
|
124
|
+
echo ""
|
|
125
|
+
|
|
126
|
+
# --- Summary ---
|
|
127
|
+
echo "========================"
|
|
128
|
+
TOTAL=$((PASS + FAIL))
|
|
129
|
+
echo "Results: $PASS/$TOTAL passed"
|
|
130
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
131
|
+
echo "FAILURES: $FAIL"
|
|
132
|
+
exit 1
|
|
133
|
+
else
|
|
134
|
+
echo "All tests passed!"
|
|
135
|
+
fi
|