failsafe-hook 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohamed Azahrioui
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,313 @@
1
+ Metadata-Version: 2.4
2
+ Name: failsafe-hook
3
+ Version: 1.0.0
4
+ Summary: Zero-config agent seatbelt for Claude Code
5
+ Author-email: Mohamed Azahrioui <mohamedazahrioui2006@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/MoAz06/FailSafe
8
+ Project-URL: Bug Tracker, https://github.com/MoAz06/FailSafe/issues
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # FailSafe
15
+
16
+ **The zero-config agent seatbelt for Claude Code.**
17
+
18
+ When you run an AI agent in autonomous mode, one bad command can delete your home directory, drop a production database, or install malware from a hallucinated package name. Claude Code has a permissions system for this -- but `--dangerously-skip-permissions` turns it all off.
19
+
20
+ FailSafe runs as a `PreToolUse` hook. **A hook deny still fires in bypass mode.** That is the whole point.
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ Install the package from PyPI:
27
+
28
+ ```bash
29
+ python -m pip install failsafe-hook
30
+ ```
31
+
32
+ Or install it from GitHub:
33
+
34
+ ```bash
35
+ python -m pip install "git+https://github.com/MoAz06/FailSafe.git"
36
+ ```
37
+
38
+ Then wire FailSafe into Claude Code:
39
+
40
+ ```bash
41
+ python -m failsafe_hook
42
+ ```
43
+
44
+ If your Python Scripts directory is on PATH, this shortcut also works:
45
+
46
+ ```bash
47
+ failsafe
48
+ ```
49
+
50
+ If Windows says `failsafe` is not found, use `python -m failsafe_hook`. That
51
+ always works.
52
+
53
+ **From source:**
54
+
55
+ ```bash
56
+ git clone https://github.com/MoAz06/FailSafe.git
57
+ cd FailSafe
58
+ python install.py
59
+ ```
60
+
61
+ `install.py` runs `pip install -e .` automatically if needed, then wires the hook.
62
+
63
+ That's it. FailSafe now runs automatically on every Claude Code session, including bypass mode.
64
+
65
+ **Check status / uninstall:**
66
+
67
+ ```bash
68
+ python -m failsafe_hook --check
69
+ python -m failsafe_hook --uninstall
70
+ ```
71
+
72
+ **Requirements:** Python 3.8+ on your PATH. No other dependencies.
73
+
74
+ ---
75
+
76
+ ## What it blocks
77
+
78
+ **Module 1 -- Slopsquatting**
79
+
80
+ AI assistants hallucinate package names. Attackers pre-register those names with malware and wait. This is called slopsquatting. In 2025, an estimated 28% of malicious packages were LLM-hallucinated versions of real ones, and 43% of hallucinated names recur on every run of the same prompt.
81
+
82
+ | Situation | Action |
83
+ | :-- | :-- |
84
+ | Package **does not exist** on npm / PyPI / crates.io / RubyGems / Go proxy | Deny |
85
+ | Exists but **one edit away** from a popular package (`expres` vs `express`) | Ask |
86
+ | Package exists but **< 90 days old** with **< 100 downloads/month** (npm/PyPI), **< 500** (Cargo), **< 1 000** total (RubyGems) | Ask |
87
+ | Everything else | Allow (silent) |
88
+
89
+ **Module 2 -- Destructive commands**
90
+
91
+ | Situation | Action |
92
+ | :-- | :-- |
93
+ | `rm -rf` on `/`, `~`, `$HOME`, `/etc`, `/usr`, `/home/<user>`, system dirs, or root globs | Deny |
94
+ | `rm -rf .git` (destroys repo history) | Deny |
95
+ | Windows paths: `/c/`, `/c/Users`, `$USERPROFILE`, `$WINDIR`, `$SYSTEMROOT` | Deny |
96
+ | `rm -rf` on relative paths, `/tmp/...`, or deep subdirectories | Allow |
97
+
98
+ **Module 3 -- One-off runners**
99
+
100
+ | Situation | Action |
101
+ | :-- | :-- |
102
+ | `npx`, `npm exec`, `pnpm dlx`, `bunx`, `bun x`, `yarn dlx` runs a non-existent package | Deny |
103
+ | Runner target is a look-alike or suspiciously fresh | Ask |
104
+ | Local paths such as `npx ./scripts/tool.js` | Allow |
105
+
106
+ **Module 4 -- Manifest installs**
107
+
108
+ A bare install pulls every dependency from a manifest file. An agent can inject a hallucinated package into `package.json` or `requirements.txt` and then run a bare install. FailSafe reads the manifest first.
109
+
110
+ | Situation | Action |
111
+ | :-- | :-- |
112
+ | `npm/pnpm/yarn/bun install` reads `package.json` -- a dep does not exist | Deny |
113
+ | `pip install -r <file>` / `uv pip install -r <file>` -- a dep does not exist | Deny |
114
+ | `poetry install` / `uv sync` reads `pyproject.toml` -- a dep does not exist | Deny |
115
+ | Local/git/url/workspace specs | Ignored |
116
+
117
+ **Module 5 -- Curl-pipe-shell**
118
+
119
+ | Situation | Action |
120
+ | :-- | :-- |
121
+ | `curl http://... \| bash` -- plain HTTP remote script | Deny |
122
+ | `curl https://... \| bash/sh/python/node/ruby` | Ask |
123
+ | `wget ... \| sh` | Ask |
124
+ | `curl ... \| grep` or piped to non-shell | Allow |
125
+
126
+ **Module 6 -- Git disaster**
127
+
128
+ | Situation | Action |
129
+ | :-- | :-- |
130
+ | `git push --force` / `git push origin +main` to `main`, `master`, `production`, `release`, `prod`, `staging` | Deny |
131
+ | `git push --force` / `+refspec` to any other branch, or `--force-with-lease` | Ask |
132
+ | `git reset --hard` or `--merge` | Ask |
133
+ | `git clean -f` / `-fd` / `-fdx` (without dry-run) | Ask |
134
+ | `git branch -D <branch>` | Ask |
135
+ | Normal git operations | Allow |
136
+
137
+ **Module 7 -- Cloud / infra blast radius**
138
+
139
+ | Situation | Action |
140
+ | :-- | :-- |
141
+ | `terraform destroy` | Ask |
142
+ | `kubectl delete namespace` / `--all` / `-A` | Ask |
143
+ | `docker system prune -a` | Ask |
144
+ | `docker volume rm` | Ask |
145
+ | `aws s3 rm --recursive` / `sync --delete` | Ask |
146
+ | `gcloud projects delete` | Ask |
147
+ | `az group delete` | Ask |
148
+ | Normal read/plan operations | Allow |
149
+
150
+ **Module 8 -- Secrets exfiltration**
151
+
152
+ | Situation | Action |
153
+ | :-- | :-- |
154
+ | `cat .env \| curl` / `cat ~/.ssh/id_rsa \| nc` | Ask |
155
+ | `curl -d @.env https://...` / `curl -F file=@.env` | Ask |
156
+ | `scp .env user@host:.` / `rsync .env user@host:` | Ask |
157
+ | `tar czf - ~/.ssh \| curl --data-binary @- https://...` | Ask |
158
+ | `aws s3 cp .env s3://bucket` / `aws s3 mv id_rsa s3://...` | Ask |
159
+ | Reading `.env` locally without network | Allow |
160
+
161
+ Sensitive file patterns: `.env`, `.env.*`, `~/.ssh/*`, `*.pem`, `*.key`, `*.p12`, `id_rsa`, `id_ed25519`, `.netrc`, `.npmrc`, `.aws/credentials`.
162
+
163
+ **Module 9 -- Cargo, Go, and RubyGems**
164
+
165
+ | Situation | Action |
166
+ | :-- | :-- |
167
+ | `cargo add` / `cargo install` -- crate not found on crates.io | Deny |
168
+ | Crate one edit away from a popular crate (`serde_jso` vs `serde_json`) | Ask |
169
+ | Crate < 90 days old with < 500 recent downloads | Ask |
170
+ | `go get` / `go install` -- module not found on the Go module proxy | Deny |
171
+ | `gem install` / `bundle add` -- gem not found on RubyGems | Deny |
172
+ | Gem one edit away from a popular gem (`railss` vs `rails`) | Ask |
173
+ | Gem < 90 days old with < 1 000 total downloads | Ask |
174
+ | `--git`, `--path`, or local `.gem` file sources | Ignored (not registry installs) |
175
+ | Private/unknown Go module domains | Ignored (avoids false positives) |
176
+
177
+ **Configuration (`failsafe.toml`)**
178
+
179
+ Place a `failsafe.toml` in your project root (or `~/.config/failsafe/config.toml`) to tune behavior:
180
+
181
+ ```toml
182
+ [failsafe]
183
+ strict = false # true: upgrade all "ask" to "deny"
184
+ protected_branches = ["main", "prod"] # replaces the default protected list
185
+ allowed_packages = ["my-internal-tool"] # always allow these names
186
+ ```
187
+
188
+ All settings are optional. Zero-config remains the default.
189
+
190
+ ---
191
+
192
+ ## Blocked command examples
193
+
194
+ ```
195
+ $ echo '{"tool_name":"Bash","tool_input":{"command":"npm install totally-not-real-pkg-xyz123"}}' \
196
+ | python hooks/failsafe.py
197
+
198
+ permissionDecision: deny
199
+ reason: FailSafe blocked this install:
200
+ - totally-not-real-pkg-xyz123: not found on the npm registry
201
+
202
+ AI assistants sometimes invent package names that don't exist; attackers
203
+ pre-register those names with malware ("slopsquatting"). Verify the correct
204
+ name on the official registry before installing.
205
+ ```
206
+
207
+ ```
208
+ $ echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
209
+ | python hooks/failsafe.py
210
+
211
+ permissionDecision: deny
212
+ reason: FailSafe blocked a destructive command:
213
+ rm -rf /
214
+ This would permanently delete the filesystem root (/).
215
+ ```
216
+
217
+ ```
218
+ $ echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main --force"}}' \
219
+ | python hooks/failsafe.py
220
+
221
+ permissionDecision: deny
222
+ reason: FailSafe blocked a force push to a protected branch:
223
+ git push origin main --force
224
+ Force pushing to 'main' permanently overwrites remote history.
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Design principles
230
+
231
+ - **Fails open** -- any network error or unexpected failure allows the action. A guard that breaks your workflow gets uninstalled.
232
+ - **Fast path** -- non-network rules (rm, git, cloud, secrets) return instantly with zero I/O.
233
+ - **Conservative** -- only near-certain danger hard-blocks. Fuzzy signals escalate to a prompt.
234
+ - **Zero dependencies** -- pure Python 3 standard library.
235
+
236
+ ---
237
+
238
+ ## How it works
239
+
240
+ ```
241
+ PreToolUse hook -> parse the Bash command
242
+ -> run instant checks (rm, git, cloud, curl-pipe, secrets)
243
+ -> for install commands, query npm / PyPI registries
244
+ -> deny if dangerous / ask if suspicious / allow otherwise
245
+ ```
246
+
247
+ `hooks/hooks.json` wires the hook on the `Bash` tool. `hooks/failsafe.py` does the work.
248
+
249
+ ---
250
+
251
+ ## What FailSafe will not do
252
+
253
+ - Score package reputation the way Socket.dev or Snyk do -- it checks existence and simple heuristics, not full supply chain analysis.
254
+ - Catch every possible dangerous command -- it targets the patterns agents actually produce, not a complete policy engine.
255
+ - Protect against a compromised package that already exists on the registry.
256
+ - Inspect `pnpm-lock.yaml` (no stdlib YAML parser).
257
+ - Parse `pyproject.toml` or `failsafe.toml` on Python < 3.11 (no `tomllib` -- fails open).
258
+ - Catch runner command strings that embed the package inside a shell snippet (`npm exec -c "eslint ."`).
259
+
260
+ ---
261
+
262
+ ## Requirements
263
+
264
+ - **Python 3.8+** on your PATH (`python3` or `python`).
265
+
266
+ ---
267
+
268
+ ## Scope: what it checks
269
+
270
+ **Install commands:** `npm install|i|add`, `pnpm add|install|i`, `yarn add`, `bun add|install|i`, `pip install`, `pip3 install`, `python -m pip install`, `poetry add`, `uv add`, `uv pip install`, `cargo add`, `cargo install`, `go get`, `go install`, `gem install`, `bundle add`
271
+
272
+ **One-off runners:** `npx`, `npm exec|x`, `pnpm dlx`, `bunx`, `bun x`, `yarn dlx`
273
+
274
+ **Manifest/lockfiles on bare install:** `package.json`, `package-lock.json` v2/v3 (npm ci), `yarn.lock` v1, `requirements.txt`, `pyproject.toml`, `poetry.lock`
275
+
276
+ Handles: quoted names, `;` and `&&` operators, `env FOO=bar` prefixes, wrapper flags (`sudo -n`, `env -i`, `time -p`), `bash -c "..."` inner commands.
277
+
278
+ Ignores: local paths, git URLs, `.tgz`/`.whl` files.
279
+
280
+ ---
281
+
282
+ ## Limitations
283
+
284
+ - npm, PyPI, crates.io, RubyGems, and Go proxy. Maven is next.
285
+ - Look-alike detection uses a curated list of popular packages, not a full corpus.
286
+ - `pnpm-lock.yaml` not inspected (no stdlib YAML parser).
287
+ - Deeply embedded runner command strings are not inspected (`npm exec -c "eslint ."`).
288
+ - `pyproject.toml`, `poetry.lock`, and `failsafe.toml` require Python 3.11+ (`tomllib`); fails open on older Python.
289
+
290
+ ---
291
+
292
+ ## Contributing
293
+
294
+ To add a new rule:
295
+
296
+ 1. Add the detection logic to `hooks/failsafe.py` as a new `check_*` function.
297
+ 2. Wire it into `main()` before the registry lookups (if it needs no network) or after (if it does).
298
+ 3. Add a `hooks/test_<name>.py` with PASS/FAIL cases following the existing pattern.
299
+ 4. Add an e2e case to `hooks/test_e2e.py`.
300
+ 5. Run all tests: all existing 265+ cases must still pass.
301
+
302
+ To add a new package ecosystem:
303
+
304
+ 1. Add a `check_<ecosystem>` registry function following `check_npm` / `check_pypi`.
305
+ 2. Extend `parse_install_targets` to recognise the new package manager.
306
+ 3. Add look-alike detection if a popular-package list exists for the ecosystem.
307
+ 4. Add focused tests.
308
+
309
+ ---
310
+
311
+ ## License
312
+
313
+ MIT
@@ -0,0 +1,300 @@
1
+ # FailSafe
2
+
3
+ **The zero-config agent seatbelt for Claude Code.**
4
+
5
+ When you run an AI agent in autonomous mode, one bad command can delete your home directory, drop a production database, or install malware from a hallucinated package name. Claude Code has a permissions system for this -- but `--dangerously-skip-permissions` turns it all off.
6
+
7
+ FailSafe runs as a `PreToolUse` hook. **A hook deny still fires in bypass mode.** That is the whole point.
8
+
9
+ ---
10
+
11
+ ## Install
12
+
13
+ Install the package from PyPI:
14
+
15
+ ```bash
16
+ python -m pip install failsafe-hook
17
+ ```
18
+
19
+ Or install it from GitHub:
20
+
21
+ ```bash
22
+ python -m pip install "git+https://github.com/MoAz06/FailSafe.git"
23
+ ```
24
+
25
+ Then wire FailSafe into Claude Code:
26
+
27
+ ```bash
28
+ python -m failsafe_hook
29
+ ```
30
+
31
+ If your Python Scripts directory is on PATH, this shortcut also works:
32
+
33
+ ```bash
34
+ failsafe
35
+ ```
36
+
37
+ If Windows says `failsafe` is not found, use `python -m failsafe_hook`. That
38
+ always works.
39
+
40
+ **From source:**
41
+
42
+ ```bash
43
+ git clone https://github.com/MoAz06/FailSafe.git
44
+ cd FailSafe
45
+ python install.py
46
+ ```
47
+
48
+ `install.py` runs `pip install -e .` automatically if needed, then wires the hook.
49
+
50
+ That's it. FailSafe now runs automatically on every Claude Code session, including bypass mode.
51
+
52
+ **Check status / uninstall:**
53
+
54
+ ```bash
55
+ python -m failsafe_hook --check
56
+ python -m failsafe_hook --uninstall
57
+ ```
58
+
59
+ **Requirements:** Python 3.8+ on your PATH. No other dependencies.
60
+
61
+ ---
62
+
63
+ ## What it blocks
64
+
65
+ **Module 1 -- Slopsquatting**
66
+
67
+ AI assistants hallucinate package names. Attackers pre-register those names with malware and wait. This is called slopsquatting. In 2025, an estimated 28% of malicious packages were LLM-hallucinated versions of real ones, and 43% of hallucinated names recur on every run of the same prompt.
68
+
69
+ | Situation | Action |
70
+ | :-- | :-- |
71
+ | Package **does not exist** on npm / PyPI / crates.io / RubyGems / Go proxy | Deny |
72
+ | Exists but **one edit away** from a popular package (`expres` vs `express`) | Ask |
73
+ | Package exists but **< 90 days old** with **< 100 downloads/month** (npm/PyPI), **< 500** (Cargo), **< 1 000** total (RubyGems) | Ask |
74
+ | Everything else | Allow (silent) |
75
+
76
+ **Module 2 -- Destructive commands**
77
+
78
+ | Situation | Action |
79
+ | :-- | :-- |
80
+ | `rm -rf` on `/`, `~`, `$HOME`, `/etc`, `/usr`, `/home/<user>`, system dirs, or root globs | Deny |
81
+ | `rm -rf .git` (destroys repo history) | Deny |
82
+ | Windows paths: `/c/`, `/c/Users`, `$USERPROFILE`, `$WINDIR`, `$SYSTEMROOT` | Deny |
83
+ | `rm -rf` on relative paths, `/tmp/...`, or deep subdirectories | Allow |
84
+
85
+ **Module 3 -- One-off runners**
86
+
87
+ | Situation | Action |
88
+ | :-- | :-- |
89
+ | `npx`, `npm exec`, `pnpm dlx`, `bunx`, `bun x`, `yarn dlx` runs a non-existent package | Deny |
90
+ | Runner target is a look-alike or suspiciously fresh | Ask |
91
+ | Local paths such as `npx ./scripts/tool.js` | Allow |
92
+
93
+ **Module 4 -- Manifest installs**
94
+
95
+ A bare install pulls every dependency from a manifest file. An agent can inject a hallucinated package into `package.json` or `requirements.txt` and then run a bare install. FailSafe reads the manifest first.
96
+
97
+ | Situation | Action |
98
+ | :-- | :-- |
99
+ | `npm/pnpm/yarn/bun install` reads `package.json` -- a dep does not exist | Deny |
100
+ | `pip install -r <file>` / `uv pip install -r <file>` -- a dep does not exist | Deny |
101
+ | `poetry install` / `uv sync` reads `pyproject.toml` -- a dep does not exist | Deny |
102
+ | Local/git/url/workspace specs | Ignored |
103
+
104
+ **Module 5 -- Curl-pipe-shell**
105
+
106
+ | Situation | Action |
107
+ | :-- | :-- |
108
+ | `curl http://... \| bash` -- plain HTTP remote script | Deny |
109
+ | `curl https://... \| bash/sh/python/node/ruby` | Ask |
110
+ | `wget ... \| sh` | Ask |
111
+ | `curl ... \| grep` or piped to non-shell | Allow |
112
+
113
+ **Module 6 -- Git disaster**
114
+
115
+ | Situation | Action |
116
+ | :-- | :-- |
117
+ | `git push --force` / `git push origin +main` to `main`, `master`, `production`, `release`, `prod`, `staging` | Deny |
118
+ | `git push --force` / `+refspec` to any other branch, or `--force-with-lease` | Ask |
119
+ | `git reset --hard` or `--merge` | Ask |
120
+ | `git clean -f` / `-fd` / `-fdx` (without dry-run) | Ask |
121
+ | `git branch -D <branch>` | Ask |
122
+ | Normal git operations | Allow |
123
+
124
+ **Module 7 -- Cloud / infra blast radius**
125
+
126
+ | Situation | Action |
127
+ | :-- | :-- |
128
+ | `terraform destroy` | Ask |
129
+ | `kubectl delete namespace` / `--all` / `-A` | Ask |
130
+ | `docker system prune -a` | Ask |
131
+ | `docker volume rm` | Ask |
132
+ | `aws s3 rm --recursive` / `sync --delete` | Ask |
133
+ | `gcloud projects delete` | Ask |
134
+ | `az group delete` | Ask |
135
+ | Normal read/plan operations | Allow |
136
+
137
+ **Module 8 -- Secrets exfiltration**
138
+
139
+ | Situation | Action |
140
+ | :-- | :-- |
141
+ | `cat .env \| curl` / `cat ~/.ssh/id_rsa \| nc` | Ask |
142
+ | `curl -d @.env https://...` / `curl -F file=@.env` | Ask |
143
+ | `scp .env user@host:.` / `rsync .env user@host:` | Ask |
144
+ | `tar czf - ~/.ssh \| curl --data-binary @- https://...` | Ask |
145
+ | `aws s3 cp .env s3://bucket` / `aws s3 mv id_rsa s3://...` | Ask |
146
+ | Reading `.env` locally without network | Allow |
147
+
148
+ Sensitive file patterns: `.env`, `.env.*`, `~/.ssh/*`, `*.pem`, `*.key`, `*.p12`, `id_rsa`, `id_ed25519`, `.netrc`, `.npmrc`, `.aws/credentials`.
149
+
150
+ **Module 9 -- Cargo, Go, and RubyGems**
151
+
152
+ | Situation | Action |
153
+ | :-- | :-- |
154
+ | `cargo add` / `cargo install` -- crate not found on crates.io | Deny |
155
+ | Crate one edit away from a popular crate (`serde_jso` vs `serde_json`) | Ask |
156
+ | Crate < 90 days old with < 500 recent downloads | Ask |
157
+ | `go get` / `go install` -- module not found on the Go module proxy | Deny |
158
+ | `gem install` / `bundle add` -- gem not found on RubyGems | Deny |
159
+ | Gem one edit away from a popular gem (`railss` vs `rails`) | Ask |
160
+ | Gem < 90 days old with < 1 000 total downloads | Ask |
161
+ | `--git`, `--path`, or local `.gem` file sources | Ignored (not registry installs) |
162
+ | Private/unknown Go module domains | Ignored (avoids false positives) |
163
+
164
+ **Configuration (`failsafe.toml`)**
165
+
166
+ Place a `failsafe.toml` in your project root (or `~/.config/failsafe/config.toml`) to tune behavior:
167
+
168
+ ```toml
169
+ [failsafe]
170
+ strict = false # true: upgrade all "ask" to "deny"
171
+ protected_branches = ["main", "prod"] # replaces the default protected list
172
+ allowed_packages = ["my-internal-tool"] # always allow these names
173
+ ```
174
+
175
+ All settings are optional. Zero-config remains the default.
176
+
177
+ ---
178
+
179
+ ## Blocked command examples
180
+
181
+ ```
182
+ $ echo '{"tool_name":"Bash","tool_input":{"command":"npm install totally-not-real-pkg-xyz123"}}' \
183
+ | python hooks/failsafe.py
184
+
185
+ permissionDecision: deny
186
+ reason: FailSafe blocked this install:
187
+ - totally-not-real-pkg-xyz123: not found on the npm registry
188
+
189
+ AI assistants sometimes invent package names that don't exist; attackers
190
+ pre-register those names with malware ("slopsquatting"). Verify the correct
191
+ name on the official registry before installing.
192
+ ```
193
+
194
+ ```
195
+ $ echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' \
196
+ | python hooks/failsafe.py
197
+
198
+ permissionDecision: deny
199
+ reason: FailSafe blocked a destructive command:
200
+ rm -rf /
201
+ This would permanently delete the filesystem root (/).
202
+ ```
203
+
204
+ ```
205
+ $ echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main --force"}}' \
206
+ | python hooks/failsafe.py
207
+
208
+ permissionDecision: deny
209
+ reason: FailSafe blocked a force push to a protected branch:
210
+ git push origin main --force
211
+ Force pushing to 'main' permanently overwrites remote history.
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Design principles
217
+
218
+ - **Fails open** -- any network error or unexpected failure allows the action. A guard that breaks your workflow gets uninstalled.
219
+ - **Fast path** -- non-network rules (rm, git, cloud, secrets) return instantly with zero I/O.
220
+ - **Conservative** -- only near-certain danger hard-blocks. Fuzzy signals escalate to a prompt.
221
+ - **Zero dependencies** -- pure Python 3 standard library.
222
+
223
+ ---
224
+
225
+ ## How it works
226
+
227
+ ```
228
+ PreToolUse hook -> parse the Bash command
229
+ -> run instant checks (rm, git, cloud, curl-pipe, secrets)
230
+ -> for install commands, query npm / PyPI registries
231
+ -> deny if dangerous / ask if suspicious / allow otherwise
232
+ ```
233
+
234
+ `hooks/hooks.json` wires the hook on the `Bash` tool. `hooks/failsafe.py` does the work.
235
+
236
+ ---
237
+
238
+ ## What FailSafe will not do
239
+
240
+ - Score package reputation the way Socket.dev or Snyk do -- it checks existence and simple heuristics, not full supply chain analysis.
241
+ - Catch every possible dangerous command -- it targets the patterns agents actually produce, not a complete policy engine.
242
+ - Protect against a compromised package that already exists on the registry.
243
+ - Inspect `pnpm-lock.yaml` (no stdlib YAML parser).
244
+ - Parse `pyproject.toml` or `failsafe.toml` on Python < 3.11 (no `tomllib` -- fails open).
245
+ - Catch runner command strings that embed the package inside a shell snippet (`npm exec -c "eslint ."`).
246
+
247
+ ---
248
+
249
+ ## Requirements
250
+
251
+ - **Python 3.8+** on your PATH (`python3` or `python`).
252
+
253
+ ---
254
+
255
+ ## Scope: what it checks
256
+
257
+ **Install commands:** `npm install|i|add`, `pnpm add|install|i`, `yarn add`, `bun add|install|i`, `pip install`, `pip3 install`, `python -m pip install`, `poetry add`, `uv add`, `uv pip install`, `cargo add`, `cargo install`, `go get`, `go install`, `gem install`, `bundle add`
258
+
259
+ **One-off runners:** `npx`, `npm exec|x`, `pnpm dlx`, `bunx`, `bun x`, `yarn dlx`
260
+
261
+ **Manifest/lockfiles on bare install:** `package.json`, `package-lock.json` v2/v3 (npm ci), `yarn.lock` v1, `requirements.txt`, `pyproject.toml`, `poetry.lock`
262
+
263
+ Handles: quoted names, `;` and `&&` operators, `env FOO=bar` prefixes, wrapper flags (`sudo -n`, `env -i`, `time -p`), `bash -c "..."` inner commands.
264
+
265
+ Ignores: local paths, git URLs, `.tgz`/`.whl` files.
266
+
267
+ ---
268
+
269
+ ## Limitations
270
+
271
+ - npm, PyPI, crates.io, RubyGems, and Go proxy. Maven is next.
272
+ - Look-alike detection uses a curated list of popular packages, not a full corpus.
273
+ - `pnpm-lock.yaml` not inspected (no stdlib YAML parser).
274
+ - Deeply embedded runner command strings are not inspected (`npm exec -c "eslint ."`).
275
+ - `pyproject.toml`, `poetry.lock`, and `failsafe.toml` require Python 3.11+ (`tomllib`); fails open on older Python.
276
+
277
+ ---
278
+
279
+ ## Contributing
280
+
281
+ To add a new rule:
282
+
283
+ 1. Add the detection logic to `hooks/failsafe.py` as a new `check_*` function.
284
+ 2. Wire it into `main()` before the registry lookups (if it needs no network) or after (if it does).
285
+ 3. Add a `hooks/test_<name>.py` with PASS/FAIL cases following the existing pattern.
286
+ 4. Add an e2e case to `hooks/test_e2e.py`.
287
+ 5. Run all tests: all existing 265+ cases must still pass.
288
+
289
+ To add a new package ecosystem:
290
+
291
+ 1. Add a `check_<ecosystem>` registry function following `check_npm` / `check_pypi`.
292
+ 2. Extend `parse_install_targets` to recognise the new package manager.
293
+ 3. Add look-alike detection if a popular-package list exists for the ecosystem.
294
+ 4. Add focused tests.
295
+
296
+ ---
297
+
298
+ ## License
299
+
300
+ MIT
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "failsafe-hook"
7
+ version = "1.0.0"
8
+ description = "Zero-config agent seatbelt for Claude Code"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [{name = "Mohamed Azahrioui", email = "mohamedazahrioui2006@gmail.com"}]
13
+
14
+ [project.scripts]
15
+ failsafe = "failsafe_hook.__main__:main"
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/MoAz06/FailSafe"
19
+ "Bug Tracker" = "https://github.com/MoAz06/FailSafe/issues"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+
4
+
5
+ def main():
6
+ args = sys.argv[1:]
7
+ if args or sys.stdin.isatty():
8
+ from failsafe_hook._install import main as _main
9
+ else:
10
+ from failsafe_hook.core import main as _main
11
+ _main()
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()