cc-safe-setup 1.1.2 → 1.2.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/README.md CHANGED
@@ -6,14 +6,14 @@
6
6
 
7
7
  **One command to make Claude Code safe for autonomous operation.**
8
8
 
9
+ Not just a destructive command blocker — 7 hooks covering safety, quality, monitoring, and developer experience.
10
+
9
11
  ```bash
10
12
  npx cc-safe-setup
11
13
  ```
12
14
 
13
15
  Installs 7 production-tested safety hooks in ~10 seconds. Zero dependencies. No manual configuration.
14
16
 
15
- <img src="demo.svg" alt="cc-safe-setup demo" width="600">
16
-
17
17
  ```
18
18
  cc-safe-setup
19
19
  Make Claude Code safe for autonomous operation
@@ -75,6 +75,19 @@ Safe to run multiple times. Existing settings are preserved. A backup is created
75
75
 
76
76
  **Requires:** [jq](https://jqlang.github.io/jq/) for JSON parsing (`brew install jq` / `apt install jq`).
77
77
 
78
+ ## Before / After
79
+
80
+ Run `npx cc-health-check` to see the difference:
81
+
82
+ | | Before | After |
83
+ |---|--------|-------|
84
+ | Safety Guards | 25% | **75%** |
85
+ | Overall Score | 50/100 | **95/100** |
86
+ | Destructive commands | Unprotected | Blocked |
87
+ | Force push | Allowed | Blocked |
88
+ | `.env` in git | Possible | Blocked |
89
+ | Context warnings | None | 4-stage alerts |
90
+
78
91
  ## Configuration
79
92
 
80
93
  | Variable | Hook | Default |
@@ -98,7 +111,7 @@ npx cc-health-check
98
111
 
99
112
  cc-safe-setup gives you 7 essential hooks. For the complete autonomous operation toolkit:
100
113
 
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.
114
+ **[Claude Code Ops Kit](https://yurukusa.github.io/cc-ops-kit-landing/?utm_source=github&utm_medium=readme&utm_campaign=safe-setup)** — 16 hooks + 6 templates + 3 exclusive tools + install.sh. Production-ready in 15 minutes.
102
115
 
103
116
  Or start with the free hooks: [claude-code-hooks](https://github.com/yurukusa/claude-code-hooks)
104
117
 
@@ -107,6 +120,12 @@ Or start with the free hooks: [claude-code-hooks](https://github.com/yurukusa/cl
107
120
  - [Japanese guide (Qiita)](https://qiita.com/yurukusa/items/a9714b33f5d974e8f1e8) — この記事の日本語解説
108
121
  - [The incident that inspired this tool](https://github.com/anthropics/claude-code/issues/36339) — NTFS junction rm -rf
109
122
 
123
+ ## Contributing
124
+
125
+ Found a false positive? Open an [issue](https://github.com/yurukusa/cc-safe-setup/issues/new?template=false_positive.md). Want a new hook? Open a [feature request](https://github.com/yurukusa/cc-safe-setup/issues/new?template=bug_report.md).
126
+
127
+ If cc-safe-setup saved your project from a destructive command, consider giving it a star — it helps others find this tool.
128
+
110
129
  ## License
111
130
 
112
131
  MIT
package/index.mjs CHANGED
@@ -58,6 +58,7 @@ const HOOKS = {
58
58
  };
59
59
 
60
60
  const HELP = process.argv.includes('--help') || process.argv.includes('-h');
61
+ const STATUS = process.argv.includes('--status') || process.argv.includes('-s');
61
62
 
62
63
  if (HELP) {
63
64
  console.log(`
@@ -65,6 +66,7 @@ if (HELP) {
65
66
 
66
67
  Usage:
67
68
  npx cc-safe-setup Install 7 safety hooks
69
+ npx cc-safe-setup --status Check installed hooks
68
70
  npx cc-safe-setup --dry-run Preview without installing
69
71
  npx cc-safe-setup --uninstall Remove all installed hooks
70
72
  npx cc-safe-setup --help Show this help
@@ -135,8 +137,48 @@ async function uninstall() {
135
137
  console.log();
136
138
  }
137
139
 
140
+ function status() {
141
+ console.log();
142
+ console.log(c.bold + ' cc-safe-setup --status' + c.reset);
143
+ console.log();
144
+
145
+ let installed = 0;
146
+ let missing = 0;
147
+ for (const [id, hook] of Object.entries(HOOKS)) {
148
+ const hookPath = join(HOOKS_DIR, id + '.sh');
149
+ if (existsSync(hookPath)) {
150
+ console.log(' ' + c.green + '✓' + c.reset + ' ' + hook.name + c.dim + ' → ' + hookPath + c.reset);
151
+ installed++;
152
+ } else {
153
+ console.log(' ' + c.red + '✗' + c.reset + ' ' + hook.name + c.dim + ' (not installed)' + c.reset);
154
+ missing++;
155
+ }
156
+ }
157
+
158
+ // Check settings.json
159
+ let settingsOk = false;
160
+ if (existsSync(SETTINGS_PATH)) {
161
+ try {
162
+ const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
163
+ if (settings.hooks) settingsOk = true;
164
+ } catch(e) {}
165
+ }
166
+ console.log();
167
+ console.log(' ' + (settingsOk ? c.green + '✓' : c.red + '✗') + c.reset + ' settings.json ' + (settingsOk ? 'has hooks configured' : 'missing hook configuration'));
168
+
169
+ console.log();
170
+ if (missing === 0) {
171
+ console.log(c.bold + ' All ' + installed + ' hooks installed.' + c.reset);
172
+ } else {
173
+ console.log(c.bold + ' ' + installed + '/' + Object.keys(HOOKS).length + ' hooks installed.' + c.reset);
174
+ console.log(' ' + c.dim + 'Run: npx cc-safe-setup' + c.reset);
175
+ }
176
+ console.log();
177
+ }
178
+
138
179
  async function main() {
139
180
  if (UNINSTALL) return uninstall();
181
+ if (STATUS) return status();
140
182
 
141
183
  console.log();
142
184
  console.log(c.bold + ' cc-safe-setup' + c.reset);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-safe-setup",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
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": {
@@ -1,3 +0,0 @@
1
- github: yurukusa
2
- ko_fi: yurukusa
3
- custom: ["https://yurukusa.github.io/cc-ops-kit-landing/?ref=safe-setup"]
@@ -1,20 +0,0 @@
1
- ---
2
- name: Bug Report
3
- about: Something isn't working as expected
4
- title: "[BUG] "
5
- labels: bug
6
- ---
7
-
8
- **What happened?**
9
-
10
- **What did you expect?**
11
-
12
- **Environment:**
13
- - OS:
14
- - Node version:
15
- - Claude Code version:
16
- - jq installed: yes/no
17
-
18
- **Steps to reproduce:**
19
- 1. `npx cc-safe-setup`
20
- 2. ...
@@ -1,12 +0,0 @@
1
- ---
2
- name: False Positive
3
- about: A safe command was blocked
4
- title: "[FALSE POSITIVE] "
5
- labels: false-positive
6
- ---
7
-
8
- **Which hook blocked the command?**
9
-
10
- **What was the command?**
11
-
12
- **Why should it be allowed?**
@@ -1,14 +0,0 @@
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/demo.svg DELETED
@@ -1 +0,0 @@
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/test.sh DELETED
@@ -1,135 +0,0 @@
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