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.
Files changed (31) hide show
  1. amagi_hookguard-0.1.0/LICENSE +21 -0
  2. amagi_hookguard-0.1.0/PKG-INFO +240 -0
  3. amagi_hookguard-0.1.0/README.md +217 -0
  4. amagi_hookguard-0.1.0/pyproject.toml +39 -0
  5. amagi_hookguard-0.1.0/setup.cfg +4 -0
  6. amagi_hookguard-0.1.0/src/amagi_hookguard/__init__.py +3 -0
  7. amagi_hookguard-0.1.0/src/amagi_hookguard/__main__.py +5 -0
  8. amagi_hookguard-0.1.0/src/amagi_hookguard/audit.py +142 -0
  9. amagi_hookguard-0.1.0/src/amagi_hookguard/cli.py +222 -0
  10. amagi_hookguard-0.1.0/src/amagi_hookguard/config.py +132 -0
  11. amagi_hookguard-0.1.0/src/amagi_hookguard/doctor.py +396 -0
  12. amagi_hookguard-0.1.0/src/amagi_hookguard/gitcmd.py +144 -0
  13. amagi_hookguard-0.1.0/src/amagi_hookguard/hook.py +114 -0
  14. amagi_hookguard-0.1.0/src/amagi_hookguard/rules.py +419 -0
  15. amagi_hookguard-0.1.0/src/amagi_hookguard/secretfiles.py +98 -0
  16. amagi_hookguard-0.1.0/src/amagi_hookguard/settings.py +142 -0
  17. amagi_hookguard-0.1.0/src/amagi_hookguard/shell.py +213 -0
  18. amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/PKG-INFO +240 -0
  19. amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/SOURCES.txt +29 -0
  20. amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/dependency_links.txt +1 -0
  21. amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/entry_points.txt +2 -0
  22. amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/requires.txt +3 -0
  23. amagi_hookguard-0.1.0/src/amagi_hookguard.egg-info/top_level.txt +1 -0
  24. amagi_hookguard-0.1.0/tests/test_cli.py +96 -0
  25. amagi_hookguard-0.1.0/tests/test_config_audit.py +154 -0
  26. amagi_hookguard-0.1.0/tests/test_git.py +97 -0
  27. amagi_hookguard-0.1.0/tests/test_hook.py +113 -0
  28. amagi_hookguard-0.1.0/tests/test_rules.py +218 -0
  29. amagi_hookguard-0.1.0/tests/test_secretfiles.py +29 -0
  30. amagi_hookguard-0.1.0/tests/test_settings_doctor.py +170 -0
  31. 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
+ ![demo](assets/demo.gif)
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
+ ![demo](assets/demo.gif)
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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """amagi-hookguard: tested guardrail hooks for Claude Code."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())