amagi-hookguard 0.1.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.
- amagi_hookguard-0.1.0/LICENSE +21 -0
- amagi_hookguard-0.1.0/PKG-INFO +240 -0
- amagi_hookguard-0.1.0/README.md +217 -0
- amagi_hookguard-0.1.0/pyproject.toml +39 -0
- amagi_hookguard-0.1.0/setup.cfg +4 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/__init__.py +3 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/__main__.py +5 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/audit.py +142 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/cli.py +222 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/config.py +132 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/doctor.py +396 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/gitcmd.py +144 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/hook.py +114 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/rules.py +419 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/secretfiles.py +98 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/settings.py +142 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard/shell.py +213 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/PKG-INFO +240 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/SOURCES.txt +29 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/dependency_links.txt +1 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/entry_points.txt +2 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/requires.txt +3 -0
- amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/top_level.txt +1 -0
- amagi_hookguard-0.1.0/tests/test_cli.py +96 -0
- amagi_hookguard-0.1.0/tests/test_config_audit.py +154 -0
- amagi_hookguard-0.1.0/tests/test_git.py +97 -0
- amagi_hookguard-0.1.0/tests/test_hook.py +113 -0
- amagi_hookguard-0.1.0/tests/test_rules.py +218 -0
- amagi_hookguard-0.1.0/tests/test_secretfiles.py +29 -0
- amagi_hookguard-0.1.0/tests/test_settings_doctor.py +170 -0
- amagi_hookguard-0.1.0/tests/test_shell.py +81 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amagisolutionsgroup, Inc. (AmagiLabs)
|
|
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,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amagi-hookguard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tested Claude Code PreToolUse/PostToolUse hooks: block destructive shell commands, secret-file reads and force pushes to main, keep a local audit log, and check your settings.json hook wiring with `doctor`.
|
|
5
|
+
Author-email: "AmagiLabs (Amagisolutionsgroup, Inc.)" <dev@amagisolutionsgroup.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/amagilabs/amagi-hookguard
|
|
8
|
+
Project-URL: Organization, https://github.com/amagilabs
|
|
9
|
+
Keywords: claude,claude-code,hooks,guardrails,security,audit-log,cli
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Security
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# amagi-hookguard
|
|
25
|
+
|
|
26
|
+
Claude Code hooks that stop `rm -rf ~`, a force push to `main`, or a read of your `.env` before they run. One command wires them into `settings.json`. From then on every Bash, PowerShell, Read and Grep call is checked first: the dangerous ones are blocked with a reason Claude can read, and the risky ones (`git reset --hard`, `curl | sh`) open a permission prompt for you to answer. Every call is written to a local log, and `doctor` tells you why a hook in your settings isn't firing.
|
|
27
|
+
|
|
28
|
+

|
|
29
|
+
|
|
30
|
+
It has 14 rules, 252 tests and no runtime dependencies. It makes no network calls and needs no API key.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pipx install amagi-hookguard
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It isn't on PyPI yet. Until it is, install from a clone:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
git clone https://github.com/amagilabs/amagi-hookguard
|
|
42
|
+
cd amagi-hookguard
|
|
43
|
+
pipx install .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Needs Python 3.9+.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Wire it in
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
amagi-hookguard install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Wired hookguard into ~/.claude/settings.json (PreToolUse + PostToolUse, matcher "*", command `amagi-hookguard hook`).
|
|
58
|
+
Restart Claude Code (or open /hooks) so it picks up the change, then run: amagi-hookguard doctor
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It adds one `PreToolUse` and one `PostToolUse` entry and leaves your other settings and hooks alone. If the file already existed, the old copy is saved next to it as `settings.json.bak-hookguard`. Running it twice doesn't add a second entry.
|
|
62
|
+
|
|
63
|
+
- `--scope project` writes `.claude/settings.json` in the current folder, so the guard travels with the repo. `--scope local` writes `.claude/settings.local.json`.
|
|
64
|
+
- `--absolute` writes the full path to the executable, for when Claude Code's `PATH` differs from your shell's.
|
|
65
|
+
- `--dry-run` prints the new file without writing it.
|
|
66
|
+
- `amagi-hookguard uninstall` removes only hookguard's entries.
|
|
67
|
+
|
|
68
|
+
### Check the wiring
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
amagi-hookguard doctor
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
amagi-hookguard doctor 0.1.0
|
|
76
|
+
|
|
77
|
+
user ~/.claude/settings.json
|
|
78
|
+
ok valid JSON
|
|
79
|
+
ok 2 command hook(s) found
|
|
80
|
+
|
|
81
|
+
project ~/myapp/.claude/settings.json
|
|
82
|
+
info file not found
|
|
83
|
+
|
|
84
|
+
local ~/myapp/.claude/settings.local.json
|
|
85
|
+
info file not found
|
|
86
|
+
|
|
87
|
+
hookguard wiring
|
|
88
|
+
ok PreToolUse runs hookguard for Bash, Read, Grep, PowerShell (user, matcher '*')
|
|
89
|
+
ok PostToolUse logs every tool call (user, matcher '*')
|
|
90
|
+
|
|
91
|
+
self-test (runs the wired command with sample events; nothing is executed)
|
|
92
|
+
ok Bash `rm -rf ~` -> blocked (exit 2)
|
|
93
|
+
ok Read `.env` -> blocked (exit 2)
|
|
94
|
+
ok Bash `git push --force origin main` -> blocked (exit 2)
|
|
95
|
+
ok Bash `ls -la` -> allowed (exit 0)
|
|
96
|
+
ok PostToolUse event -> logged (to a scratch file; your log is untouched)
|
|
97
|
+
|
|
98
|
+
config and audit log
|
|
99
|
+
info no hookguard.json found; using defaults (protected branches: main, master)
|
|
100
|
+
info ~/.claude/hookguard/audit.jsonl does not exist yet (created on the first logged call)
|
|
101
|
+
|
|
102
|
+
Result: PASS (0 fail, 0 warn)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`doctor` reads every settings file Claude Code loads (user, project, local, and managed if present). It flags the mistakes that make a hook silently do nothing, whether or not the hook is hookguard: broken JSON, an event name in the wrong case, a matcher that can never match, a script path that doesn't exist, a timeout written as a string. The self-test then pipes sample events through the exact command your settings run, so a hook that is wired but never blocks also shows up.
|
|
106
|
+
|
|
107
|
+
The repo ships a broken file to try it on:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
amagi-hookguard doctor --settings examples/settings.broken.json --no-exec
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
amagi-hookguard doctor 0.1.0
|
|
115
|
+
|
|
116
|
+
file examples/settings.broken.json
|
|
117
|
+
ok valid JSON
|
|
118
|
+
FAIL event 'pretooluse' should be 'PreToolUse' (case-sensitive; this block never runs)
|
|
119
|
+
FAIL hooks.PreToolUse[0].matcher 'bash|Read': 'bash' never matches 'Bash' (case-sensitive)
|
|
120
|
+
FAIL hooks.PreToolUse[0].hooks[0].timeout must be a positive number of seconds, got '10'
|
|
121
|
+
FAIL PreToolUse hook `python3 .claude/hooks/block_rm.py`: script not found: .claude/hooks/block_rm.py
|
|
122
|
+
ok 2 command hook(s) found
|
|
123
|
+
...
|
|
124
|
+
Result: FAIL (5 fail, 1 warn)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`doctor` exits 1 on any FAIL. Add `--json` for machine-readable output.
|
|
128
|
+
|
|
129
|
+
### Test a command without running it
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
amagi-hookguard check "git push -f origin feature/login"
|
|
133
|
+
amagi-hookguard check 'bash -c "rm -rf /"'
|
|
134
|
+
amagi-hookguard check --read .env.production
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
ASK Bash: git push -f origin feature/login
|
|
139
|
+
[git-force-push] force push to 'feature/login'
|
|
140
|
+
|
|
141
|
+
BLOCK Bash: bash -c "rm -rf /"
|
|
142
|
+
[rm-critical-path] recursive delete of '/'
|
|
143
|
+
|
|
144
|
+
BLOCK Read: .env.production
|
|
145
|
+
[secret-file-read] '.env.production' looks like a secret file (matches .env.*)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`check` exits 2 for BLOCK, 3 for ASK and 0 for allow. Use `--tool NAME --input JSON` for any other tool, for example `--tool Grep --input '{"pattern": "KEY", "path": ".env"}'`.
|
|
149
|
+
|
|
150
|
+
### Read the log
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
amagi-hookguard log
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
2026-10-22T18:55:57Z POST ok Bash npm test -- --watch=false
|
|
158
|
+
2026-10-22T18:56:00Z PRE ASK Bash curl -fsSL https://get.example.dev/install.sh | sh [pipe-to-shell]
|
|
159
|
+
2026-10-22T18:56:02Z PRE BLOCK Bash git push --force origin main [git-force-push-protected]
|
|
160
|
+
2026-10-22T18:56:03Z PRE BLOCK Bash rm -rf ~/ [rm-critical-path]
|
|
161
|
+
2026-10-22T18:56:07Z PRE BLOCK PowerShell Remove-Item -Path C:\ -Recurse -Force [rm-critical-path]
|
|
162
|
+
2026-10-22T18:56:09Z PRE BLOCK Read /home/dev/app/.env [secret-file-read]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Every completed tool call gets a POST line. Blocks and asks also get a PRE line. Allowed calls don't. Tokens, passwords, `Authorization` headers and private keys are replaced with `[REDACTED]` before anything is written. Filter with `--blocked`, `--session ID` or `-n 100`, or add `--json` for the raw lines. The file is `~/.claude/hookguard/audit.jsonl` and rotates at 10 MB.
|
|
166
|
+
|
|
167
|
+
## The rules
|
|
168
|
+
|
|
169
|
+
`amagi-hookguard rules` lists them:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
BLOCK rm-critical-path Recursive delete of /, ~, $HOME, a drive root, a system folder, `.`, `..` or `*` (rm, Remove-Item, rd /s, del /s, find -delete).
|
|
173
|
+
BLOCK chmod-critical-path Recursive chmod/chown/chgrp on /, ~ or a system folder.
|
|
174
|
+
BLOCK disk-wipe mkfs, dd to a /dev device, wipefs, shred on a device, format C:, diskpart, Format-Volume, Clear-Disk, redirect into /dev/sdX.
|
|
175
|
+
BLOCK fork-bomb The classic :(){ :|:& };: fork bomb.
|
|
176
|
+
BLOCK git-force-push-protected Force push (-f, --force, --force-with-lease, +ref, --mirror) to a protected branch (main, master + your list), or when the branch can't be determined.
|
|
177
|
+
BLOCK git-push-delete-protected Deleting a protected branch on the remote (git push --delete main, git push origin :main).
|
|
178
|
+
BLOCK secret-file-read Reading a secret file (.env, *.pem, *.key, id_rsa, .aws/credentials, .netrc, ...) with Read, Grep or a shell command (cat, type, Get-Content, grep, cp, curl -F @file, < redirect).
|
|
179
|
+
ASK git-force-push Force push to a branch that isn't protected.
|
|
180
|
+
ASK git-reset-hard git reset --hard (throws away uncommitted work).
|
|
181
|
+
ASK git-clean-force git clean -f (deletes untracked files).
|
|
182
|
+
ASK git-discard-changes git checkout -- . / git checkout . / git restore . (throws away working-tree edits).
|
|
183
|
+
ASK pipe-to-shell Piping a download straight into a shell (curl | sh, wget | bash, iwr | iex).
|
|
184
|
+
ASK sql-drop DROP DATABASE / DROP TABLE / DROP SCHEMA / TRUNCATE TABLE in a shell command.
|
|
185
|
+
ASK system-power shutdown, reboot, halt, poweroff, Stop-Computer, Restart-Computer.
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
hookguard parses each command before it matches anything. It splits on `;`, `&&`, `||` and pipes, strips wrappers such as `sudo`, `env`, `nohup` and `timeout`, and looks inside `bash -c "..."`, `pwsh -Command`, `powershell -EncodedCommand`, `cmd /c` and `eval`. So `sudo rm -rf /` is blocked, and `find . -name '*.pyc' -delete` is allowed. For a bare `git push --force`, it asks git which branch you're on.
|
|
189
|
+
|
|
190
|
+
A BLOCK makes the hook exit 2, and Claude gets the reason:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
amagi-hookguard blocked this Bash call [rm-critical-path]: recursive delete of '~/'. If the user really wants this, they can run it themselves or disable the rule in hookguard.json.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
An ASK returns `"permissionDecision": "ask"`, so Claude Code shows you its normal permission prompt.
|
|
197
|
+
|
|
198
|
+
## Config
|
|
199
|
+
|
|
200
|
+
Optional. Put it in `~/.claude/hookguard.json` (this one is `examples/hookguard.json`):
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"protected_branches": ["main", "master", "release/*"],
|
|
205
|
+
"secret_globs": ["*.secret", "config/prod/*.yml"],
|
|
206
|
+
"secret_allow_globs": ["tests/fixtures/*.pem"],
|
|
207
|
+
"disabled_rules": ["system-power"],
|
|
208
|
+
"allow_commands": ["^rm -rf \\./tmp/scratch$"],
|
|
209
|
+
"ask_as_block": false,
|
|
210
|
+
"fail_closed": false,
|
|
211
|
+
"audit_max_bytes": 10000000
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- `ask_as_block` turns every ASK into a BLOCK, for unattended runs where nobody is there to answer a prompt.
|
|
216
|
+
- `fail_closed` blocks the call if hookguard itself crashes. By default a crash lets the call through and prints the error.
|
|
217
|
+
- `allow_commands` are regexes searched for in the command text. A match skips every rule for that command, so anchor them with `^` and `$`.
|
|
218
|
+
|
|
219
|
+
A project can add `.claude/hookguard.json` too, but by default it can only make things stricter: extra protected branches, extra secret globs, `ask_as_block`, `fail_closed`. A repo you just cloned can't switch the guard off for itself. Set `"trust_project_config": true` in your user file to let project files loosen rules as well.
|
|
220
|
+
|
|
221
|
+
## Limits
|
|
222
|
+
|
|
223
|
+
- This is pattern matching on the command text, not a sandbox. `rm -rf "$DIR"`, `python -c "import shutil; shutil.rmtree('/')"` and anything decoded at run time (`base64 -d | sh`) get through. It catches the mistakes an agent makes by accident. It won't stop code that is written to get around it.
|
|
224
|
+
- Commands sent to another machine (`ssh host 'cat ~/.ssh/id_rsa'`) aren't parsed.
|
|
225
|
+
- Secret files are protected from reads. Write and Edit calls on them aren't checked.
|
|
226
|
+
- Each tool call starts a Python process, so every call pays Python's start-up time. The check itself takes a few milliseconds.
|
|
227
|
+
|
|
228
|
+
## If this saved you time
|
|
229
|
+
|
|
230
|
+
Star the repo. It helps other people running Claude Code find it.
|
|
231
|
+
|
|
232
|
+
## More from AmagiLabs
|
|
233
|
+
|
|
234
|
+
- [amagilabs](https://github.com/amagilabs): the rest of the AmagiLabs open-source tools.
|
|
235
|
+
- [amagi-watch](https://github.com/amagilabs/amagi-watch): give Claude a video URL and get back a timestamped transcript and key frames in one markdown file.
|
|
236
|
+
- [amagi-legal-review-skills](https://github.com/amagilabs/amagi-legal-review-skills): Claude Skills for contract and lease review.
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# amagi-hookguard
|
|
2
|
+
|
|
3
|
+
Claude Code hooks that stop `rm -rf ~`, a force push to `main`, or a read of your `.env` before they run. One command wires them into `settings.json`. From then on every Bash, PowerShell, Read and Grep call is checked first: the dangerous ones are blocked with a reason Claude can read, and the risky ones (`git reset --hard`, `curl | sh`) open a permission prompt for you to answer. Every call is written to a local log, and `doctor` tells you why a hook in your settings isn't firing.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
It has 14 rules, 252 tests and no runtime dependencies. It makes no network calls and needs no API key.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pipx install amagi-hookguard
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It isn't on PyPI yet. Until it is, install from a clone:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
git clone https://github.com/amagilabs/amagi-hookguard
|
|
19
|
+
cd amagi-hookguard
|
|
20
|
+
pipx install .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Needs Python 3.9+.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Wire it in
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
amagi-hookguard install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Wired hookguard into ~/.claude/settings.json (PreToolUse + PostToolUse, matcher "*", command `amagi-hookguard hook`).
|
|
35
|
+
Restart Claude Code (or open /hooks) so it picks up the change, then run: amagi-hookguard doctor
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It adds one `PreToolUse` and one `PostToolUse` entry and leaves your other settings and hooks alone. If the file already existed, the old copy is saved next to it as `settings.json.bak-hookguard`. Running it twice doesn't add a second entry.
|
|
39
|
+
|
|
40
|
+
- `--scope project` writes `.claude/settings.json` in the current folder, so the guard travels with the repo. `--scope local` writes `.claude/settings.local.json`.
|
|
41
|
+
- `--absolute` writes the full path to the executable, for when Claude Code's `PATH` differs from your shell's.
|
|
42
|
+
- `--dry-run` prints the new file without writing it.
|
|
43
|
+
- `amagi-hookguard uninstall` removes only hookguard's entries.
|
|
44
|
+
|
|
45
|
+
### Check the wiring
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
amagi-hookguard doctor
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
amagi-hookguard doctor 0.1.0
|
|
53
|
+
|
|
54
|
+
user ~/.claude/settings.json
|
|
55
|
+
ok valid JSON
|
|
56
|
+
ok 2 command hook(s) found
|
|
57
|
+
|
|
58
|
+
project ~/myapp/.claude/settings.json
|
|
59
|
+
info file not found
|
|
60
|
+
|
|
61
|
+
local ~/myapp/.claude/settings.local.json
|
|
62
|
+
info file not found
|
|
63
|
+
|
|
64
|
+
hookguard wiring
|
|
65
|
+
ok PreToolUse runs hookguard for Bash, Read, Grep, PowerShell (user, matcher '*')
|
|
66
|
+
ok PostToolUse logs every tool call (user, matcher '*')
|
|
67
|
+
|
|
68
|
+
self-test (runs the wired command with sample events; nothing is executed)
|
|
69
|
+
ok Bash `rm -rf ~` -> blocked (exit 2)
|
|
70
|
+
ok Read `.env` -> blocked (exit 2)
|
|
71
|
+
ok Bash `git push --force origin main` -> blocked (exit 2)
|
|
72
|
+
ok Bash `ls -la` -> allowed (exit 0)
|
|
73
|
+
ok PostToolUse event -> logged (to a scratch file; your log is untouched)
|
|
74
|
+
|
|
75
|
+
config and audit log
|
|
76
|
+
info no hookguard.json found; using defaults (protected branches: main, master)
|
|
77
|
+
info ~/.claude/hookguard/audit.jsonl does not exist yet (created on the first logged call)
|
|
78
|
+
|
|
79
|
+
Result: PASS (0 fail, 0 warn)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`doctor` reads every settings file Claude Code loads (user, project, local, and managed if present). It flags the mistakes that make a hook silently do nothing, whether or not the hook is hookguard: broken JSON, an event name in the wrong case, a matcher that can never match, a script path that doesn't exist, a timeout written as a string. The self-test then pipes sample events through the exact command your settings run, so a hook that is wired but never blocks also shows up.
|
|
83
|
+
|
|
84
|
+
The repo ships a broken file to try it on:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
amagi-hookguard doctor --settings examples/settings.broken.json --no-exec
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
amagi-hookguard doctor 0.1.0
|
|
92
|
+
|
|
93
|
+
file examples/settings.broken.json
|
|
94
|
+
ok valid JSON
|
|
95
|
+
FAIL event 'pretooluse' should be 'PreToolUse' (case-sensitive; this block never runs)
|
|
96
|
+
FAIL hooks.PreToolUse[0].matcher 'bash|Read': 'bash' never matches 'Bash' (case-sensitive)
|
|
97
|
+
FAIL hooks.PreToolUse[0].hooks[0].timeout must be a positive number of seconds, got '10'
|
|
98
|
+
FAIL PreToolUse hook `python3 .claude/hooks/block_rm.py`: script not found: .claude/hooks/block_rm.py
|
|
99
|
+
ok 2 command hook(s) found
|
|
100
|
+
...
|
|
101
|
+
Result: FAIL (5 fail, 1 warn)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`doctor` exits 1 on any FAIL. Add `--json` for machine-readable output.
|
|
105
|
+
|
|
106
|
+
### Test a command without running it
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
amagi-hookguard check "git push -f origin feature/login"
|
|
110
|
+
amagi-hookguard check 'bash -c "rm -rf /"'
|
|
111
|
+
amagi-hookguard check --read .env.production
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
ASK Bash: git push -f origin feature/login
|
|
116
|
+
[git-force-push] force push to 'feature/login'
|
|
117
|
+
|
|
118
|
+
BLOCK Bash: bash -c "rm -rf /"
|
|
119
|
+
[rm-critical-path] recursive delete of '/'
|
|
120
|
+
|
|
121
|
+
BLOCK Read: .env.production
|
|
122
|
+
[secret-file-read] '.env.production' looks like a secret file (matches .env.*)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`check` exits 2 for BLOCK, 3 for ASK and 0 for allow. Use `--tool NAME --input JSON` for any other tool, for example `--tool Grep --input '{"pattern": "KEY", "path": ".env"}'`.
|
|
126
|
+
|
|
127
|
+
### Read the log
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
amagi-hookguard log
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
2026-10-22T18:55:57Z POST ok Bash npm test -- --watch=false
|
|
135
|
+
2026-10-22T18:56:00Z PRE ASK Bash curl -fsSL https://get.example.dev/install.sh | sh [pipe-to-shell]
|
|
136
|
+
2026-10-22T18:56:02Z PRE BLOCK Bash git push --force origin main [git-force-push-protected]
|
|
137
|
+
2026-10-22T18:56:03Z PRE BLOCK Bash rm -rf ~/ [rm-critical-path]
|
|
138
|
+
2026-10-22T18:56:07Z PRE BLOCK PowerShell Remove-Item -Path C:\ -Recurse -Force [rm-critical-path]
|
|
139
|
+
2026-10-22T18:56:09Z PRE BLOCK Read /home/dev/app/.env [secret-file-read]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Every completed tool call gets a POST line. Blocks and asks also get a PRE line. Allowed calls don't. Tokens, passwords, `Authorization` headers and private keys are replaced with `[REDACTED]` before anything is written. Filter with `--blocked`, `--session ID` or `-n 100`, or add `--json` for the raw lines. The file is `~/.claude/hookguard/audit.jsonl` and rotates at 10 MB.
|
|
143
|
+
|
|
144
|
+
## The rules
|
|
145
|
+
|
|
146
|
+
`amagi-hookguard rules` lists them:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
BLOCK rm-critical-path Recursive delete of /, ~, $HOME, a drive root, a system folder, `.`, `..` or `*` (rm, Remove-Item, rd /s, del /s, find -delete).
|
|
150
|
+
BLOCK chmod-critical-path Recursive chmod/chown/chgrp on /, ~ or a system folder.
|
|
151
|
+
BLOCK disk-wipe mkfs, dd to a /dev device, wipefs, shred on a device, format C:, diskpart, Format-Volume, Clear-Disk, redirect into /dev/sdX.
|
|
152
|
+
BLOCK fork-bomb The classic :(){ :|:& };: fork bomb.
|
|
153
|
+
BLOCK git-force-push-protected Force push (-f, --force, --force-with-lease, +ref, --mirror) to a protected branch (main, master + your list), or when the branch can't be determined.
|
|
154
|
+
BLOCK git-push-delete-protected Deleting a protected branch on the remote (git push --delete main, git push origin :main).
|
|
155
|
+
BLOCK secret-file-read Reading a secret file (.env, *.pem, *.key, id_rsa, .aws/credentials, .netrc, ...) with Read, Grep or a shell command (cat, type, Get-Content, grep, cp, curl -F @file, < redirect).
|
|
156
|
+
ASK git-force-push Force push to a branch that isn't protected.
|
|
157
|
+
ASK git-reset-hard git reset --hard (throws away uncommitted work).
|
|
158
|
+
ASK git-clean-force git clean -f (deletes untracked files).
|
|
159
|
+
ASK git-discard-changes git checkout -- . / git checkout . / git restore . (throws away working-tree edits).
|
|
160
|
+
ASK pipe-to-shell Piping a download straight into a shell (curl | sh, wget | bash, iwr | iex).
|
|
161
|
+
ASK sql-drop DROP DATABASE / DROP TABLE / DROP SCHEMA / TRUNCATE TABLE in a shell command.
|
|
162
|
+
ASK system-power shutdown, reboot, halt, poweroff, Stop-Computer, Restart-Computer.
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
hookguard parses each command before it matches anything. It splits on `;`, `&&`, `||` and pipes, strips wrappers such as `sudo`, `env`, `nohup` and `timeout`, and looks inside `bash -c "..."`, `pwsh -Command`, `powershell -EncodedCommand`, `cmd /c` and `eval`. So `sudo rm -rf /` is blocked, and `find . -name '*.pyc' -delete` is allowed. For a bare `git push --force`, it asks git which branch you're on.
|
|
166
|
+
|
|
167
|
+
A BLOCK makes the hook exit 2, and Claude gets the reason:
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
amagi-hookguard blocked this Bash call [rm-critical-path]: recursive delete of '~/'. If the user really wants this, they can run it themselves or disable the rule in hookguard.json.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
An ASK returns `"permissionDecision": "ask"`, so Claude Code shows you its normal permission prompt.
|
|
174
|
+
|
|
175
|
+
## Config
|
|
176
|
+
|
|
177
|
+
Optional. Put it in `~/.claude/hookguard.json` (this one is `examples/hookguard.json`):
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"protected_branches": ["main", "master", "release/*"],
|
|
182
|
+
"secret_globs": ["*.secret", "config/prod/*.yml"],
|
|
183
|
+
"secret_allow_globs": ["tests/fixtures/*.pem"],
|
|
184
|
+
"disabled_rules": ["system-power"],
|
|
185
|
+
"allow_commands": ["^rm -rf \\./tmp/scratch$"],
|
|
186
|
+
"ask_as_block": false,
|
|
187
|
+
"fail_closed": false,
|
|
188
|
+
"audit_max_bytes": 10000000
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- `ask_as_block` turns every ASK into a BLOCK, for unattended runs where nobody is there to answer a prompt.
|
|
193
|
+
- `fail_closed` blocks the call if hookguard itself crashes. By default a crash lets the call through and prints the error.
|
|
194
|
+
- `allow_commands` are regexes searched for in the command text. A match skips every rule for that command, so anchor them with `^` and `$`.
|
|
195
|
+
|
|
196
|
+
A project can add `.claude/hookguard.json` too, but by default it can only make things stricter: extra protected branches, extra secret globs, `ask_as_block`, `fail_closed`. A repo you just cloned can't switch the guard off for itself. Set `"trust_project_config": true` in your user file to let project files loosen rules as well.
|
|
197
|
+
|
|
198
|
+
## Limits
|
|
199
|
+
|
|
200
|
+
- This is pattern matching on the command text, not a sandbox. `rm -rf "$DIR"`, `python -c "import shutil; shutil.rmtree('/')"` and anything decoded at run time (`base64 -d | sh`) get through. It catches the mistakes an agent makes by accident. It won't stop code that is written to get around it.
|
|
201
|
+
- Commands sent to another machine (`ssh host 'cat ~/.ssh/id_rsa'`) aren't parsed.
|
|
202
|
+
- Secret files are protected from reads. Write and Edit calls on them aren't checked.
|
|
203
|
+
- Each tool call starts a Python process, so every call pays Python's start-up time. The check itself takes a few milliseconds.
|
|
204
|
+
|
|
205
|
+
## If this saved you time
|
|
206
|
+
|
|
207
|
+
Star the repo. It helps other people running Claude Code find it.
|
|
208
|
+
|
|
209
|
+
## More from AmagiLabs
|
|
210
|
+
|
|
211
|
+
- [amagilabs](https://github.com/amagilabs): the rest of the AmagiLabs open-source tools.
|
|
212
|
+
- [amagi-watch](https://github.com/amagilabs/amagi-watch): give Claude a video URL and get back a timestamped transcript and key frames in one markdown file.
|
|
213
|
+
- [amagi-legal-review-skills](https://github.com/amagilabs/amagi-legal-review-skills): Claude Skills for contract and lease review.
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "amagi-hookguard"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tested Claude Code PreToolUse/PostToolUse hooks: block destructive shell commands, secret-file reads and force pushes to main, keep a local audit log, and check your settings.json hook wiring with `doctor`."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "AmagiLabs (Amagisolutionsgroup, Inc.)", email = "dev@amagisolutionsgroup.com" }]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
keywords = ["claude", "claude-code", "hooks", "guardrails", "security", "audit-log", "cli"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Security",
|
|
21
|
+
"Topic :: Software Development",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=7.0"]
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
amagi-hookguard = "amagi_hookguard.cli:main"
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/amagilabs/amagi-hookguard"
|
|
33
|
+
Organization = "https://github.com/amagilabs"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|