agent-guard-hooks 0.4.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 hao li
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,391 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-guard-hooks
3
+ Version: 0.4.0
4
+ Summary: Behavior-guardrail hooks for Claude Code: test-tampering + outbound-action guards, mutation test-honesty, and post-exec verification
5
+ Author: hao li
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/hahahahahahahahah6/agent-guard
8
+ Keywords: claude-code,hooks,ai-agents,guardrails,developer-tools
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Dynamic: license-file
13
+
14
+ # agent-guard
15
+
16
+ Behavior-guardrail hooks for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
17
+ Five guards plus test-honesty CLIs, one install, zero dependencies (Python
18
+ standard library only):
19
+
20
+ - **Test-tampering guard** — stops the "green by editing the test" cheat.
21
+ On `SessionStart` it snapshots hashes of every test and source file; on
22
+ `Stop` it diffs. If test files were modified or deleted while no source
23
+ file changed, the stop is blocked until the agent proves each changed test
24
+ actually fails without the fix.
25
+ - **Outbound-action guard** — a `PreToolUse` hook on `Bash` with a denylist
26
+ of risky action patterns: pushes to protected branches (including force
27
+ pushes), package publishes (`npm publish`, `twine upload`, …), prod deploys,
28
+ cloud provisioning (spend), and mass-send channels (Slack webhooks,
29
+ mailers). Matches block with a named rule; a user allowlist in
30
+ `config.json` overrides the denylist. Every matched decision is written to
31
+ an audit log. v0.2 also scans the *content* of script files the command
32
+ executes (`bash evil.sh`), closing the "write it to a script first" bypass.
33
+ - **Post-exec read-back verifier** (v0.2) — a `PostToolUse` hook on `Bash`
34
+ that reads back world state after a command claimed an outbound effect
35
+ (`git push`, `npm publish`) and warns — never blocks — when the effect
36
+ isn't visible. Defense-in-depth on top of PreToolUse prevention: prevent
37
+ first, verify after.
38
+ - **Mutation test-honesty checker** (v0.2) — `agent-guard mutate-check`
39
+ deliberately breaks assertions (regex-based mutants for JS/TS and Python),
40
+ re-runs the tests per mutant, and reports survivors: tests that stayed
41
+ green don't actually cover the bug.
42
+ - **Comment-slop guard** (v0.3) — a `PreToolUse` hook on `Write`/`Edit` that
43
+ scores the *added* comments (never pre-existing code) for narrative slop:
44
+ commented-out code, restatements of obvious code ("This function adds two
45
+ numbers"), in-code changelogs, meta/apologetic notes, emoji, and
46
+ docstrings that just restate the signature. Blocks at a configurable
47
+ threshold, or `agent-guard decomment --check/--fix` for a one-command
48
+ decomment pass before PRs.
49
+ - **Cheat-sniffing guard** (v0.4) — catches the cheating that never touches
50
+ test files: RNG rigging (`random.seed(123)` with no reproducibility
51
+ marker, patching `random.shuffle`), mocking the function under test
52
+ instead of its collaborators, `conftest.py` plants, time-freezing, and
53
+ always-True comparison dunders. A `PreToolUse` hook on `Write`/`Edit`
54
+ blocks cheat patterns in added test text; `agent-guard cheatsniff
55
+ --check` audits the repo.
56
+
57
+ The failure modes are real, quoted from the community:
58
+
59
+ > *"Instead of fixing the indexing logic in the source file, the agent quietly
60
+ > modified the test file: it changed `expect(page.items.length).toBe(10)` to
61
+ > `toBe(9)`, re-ran the test, saw green, and told us the refactor was
62
+ > complete."* — navune, r/ClaudeCode
63
+
64
+ > *"the real risk is not a bad answer but a bad action"* — dank_as_fuck_,
65
+ > r/aiagents, on why guardrails must live *"below the prompt layer"*
66
+ > (verstands)
67
+
68
+ ## Differentiation
69
+
70
+ - **vs Rashomon** (r/aiagents): Rashomon *observes* — it records commands,
71
+ edits and failures, then compares them against the agent's closing summary.
72
+ agent-guard *prevents* — the hook sits in the tool-call path and blocks the
73
+ bad action before it happens. Observation and prevention are complementary
74
+ layers; this is the prevention one.
75
+ - **vs edit-guard** (same author): edit-guard blocks *stale* cross-session
76
+ edits (write-after-write on a file another session changed). agent-guard
77
+ blocks *misbehaving* actions (weakened tests, unapproved outbound effects).
78
+ Different failure modes, same fail-open hook philosophy.
79
+ - **vs Kvitansiya** (Show HN, 2026-10-01): Kvitansiya verifies at stop —
80
+ it checks the claimed outcome once, when the session ends. agent-guard v0.2
81
+ adds post-exec read-back as defense-in-depth *on top of* PreToolUse
82
+ prevention: block the bad action before it happens, then verify the claimed
83
+ effect is actually visible afterwards. Prevent first, verify after.
84
+
85
+ ## v0.4: cheat-sniffing beyond test files
86
+
87
+ ```bash
88
+ agent-guard cheatsniff --check tests/ # per-file cheat scores, exit 1 over threshold
89
+ ```
90
+
91
+ Half of agent cheating never touches a test file. A dev.to study (remdore,
92
+ 2026-10-01, 102 runs × 4 models) found agents patching the RNG *"so the list
93
+ would always be sorted"*, mocking the function under test instead of its
94
+ collaborators, and planting helpers in `conftest.py`. The test-tampering
95
+ guard (tests-only-change diff) and `mutate-check` (assertion mutation) both
96
+ miss this family — "restore the test files and re-run" only catches the
97
+ dumb half.
98
+
99
+ `install` registers `agent-guard hook-cheatsniff` as a `PreToolUse` hook on
100
+ `Write`/`Edit`. It fires only for test-ish files (`test_*.py`,
101
+ `*_test.py`, `conftest.py`, anything under `tests/`) and scores only the
102
+ *added* text. Six cheat kinds, regex-based and deliberately conservative:
103
+
104
+ - **mock-subject** (severe): `mock.patch("billing.total")` inside
105
+ `test_billing.py` — patching the module under test itself, not its
106
+ dependencies. Patching a collaborator (`stripe.Charge.create`) is clean.
107
+ - **rng-patch** (severe): patching `random.shuffle` / `random.random` /
108
+ `random.sample` to force outcomes.
109
+ - **conftest-patch** (severe): `conftest.py` monkeypatching the subject or
110
+ other local modules — including hand-rolled `mymod.shuffle = ...` direct
111
+ assignment (pure fixtures are clean).
112
+ - **rng-seed**: a fixed `random.seed(123)` with no reproducibility marker.
113
+ `random.seed(42)` next to a "reproducible" comment is legitimate and not
114
+ flagged — the marker is the whole difference, and it's documented.
115
+ - **time-freeze**: `freeze_time(...)`, `time.sleep` patched to a no-op.
116
+ - **weak-comparator**: a `__eq__` / `__lt__` / … whose body unconditionally
117
+ `return True`.
118
+
119
+ One severe hit reaches the default threshold (30/100) on its own. Warn mode
120
+ (`AGENT_GUARD_CHEAT_MODE=warn` or `cheat_sniff.mode=warn` in config) advises
121
+ instead of blocking. A `cheat_sniff.allow` list (`"test_sort.py:rng-seed"`,
122
+ `"*/legacy/*:*")` covers the judgment calls you disagree with. Everything
123
+ fails open.
124
+
125
+ ## v0.3: comment-slop guard
126
+
127
+ ```bash
128
+ agent-guard decomment --check src/ # per-file slop scores, exit 1 over threshold
129
+ agent-guard decomment --fix src/app.py # remove commented-out code only (writes .bak)
130
+ ```
131
+
132
+ `install` registers `agent-guard hook-commentslop` as a `PreToolUse` hook
133
+ on `Write`/`Edit`. It scores only the *added* comment lines — your existing
134
+ codebase is never punished — and blocks (exit 2) when the added-slop score
135
+ reaches the threshold (default 30/100). The block message names the lines
136
+ and the slop kind, and points at `decomment --fix`. Six slop kinds, all
137
+ regex-based and deliberately conservative (when in doubt, it doesn't flag):
138
+
139
+ - **commented-code** (weighted highest): 2+ consecutive comment lines that
140
+ look like code — dead code left in comments instead of deleted
141
+ - **restatement**: "This function …", "Here we …", "The following …"
142
+ - **changelog**: "Fixed …", "Updated …" narrating the diff inside the code
143
+ (belongs in the commit message)
144
+ - **meta-apology**: HACK, "sorry", "workaround", "fix this later", `!!!`
145
+ - **emoji** in comments
146
+ - **obvious-doc**: a docstring whose words are >70% covered by the next
147
+ line's identifiers (e.g. `"""Add a and b."""` above `def add(a, b)`)
148
+
149
+ Tool directives (`# noqa`, `# type: ignore`, `eslint-disable`, …) are never
150
+ flagged. `--fix` is surgical: it removes only `commented-code` blocks and
151
+ always writes a `.bak` backup first. Warn mode
152
+ (`AGENT_GUARD_COMMENT_MODE=warn` or `comment_slop.mode=warn` in config)
153
+ advises instead of blocking. Everything fails open.
154
+
155
+ ## v0.2: script-content inspection, mutation test-honesty, post-exec verification
156
+
157
+ ### Script-content inspection
158
+
159
+ The Bash denylist failed as soon as the agent wrote the destructive command
160
+ into a script and executed the script instead (`bash evil.sh`). The outbound
161
+ guard now extracts script files from the command — `bash|sh|zsh|dash`,
162
+ `python|node|ruby|perl|php <file>`, `./run.sh`, `source`/`. <file>`, plus
163
+ `bash -c '...'` inline code — resolves them against the hook's cwd, and scans
164
+ the file content with the same rule logic (regexes + destination-aware
165
+ `git push` parsing). Hits block with a `script-content:<rule_id>` id so the
166
+ audit log shows where the hit came from. Missing, unreadable, or >1MB files
167
+ are skipped (fail open); only plausible script extensions are scanned; the
168
+ user allowlist and `disabled_rules` apply to content hits too.
169
+
170
+ ### Mutation test-honesty checker
171
+
172
+ ```bash
173
+ agent-guard mutate-check tests/test_app.py --project-root . -- pytest -q
174
+ ```
175
+
176
+ Generates up to 20 syntactic mutations (default; `--max-mutations`), copies
177
+ the project to a temp dir per mutant (skipping `.git`/`node_modules`/etc.),
178
+ and runs the test command there. Mutation operators:
179
+
180
+ - JS/TS: `toBe(<n>)` → `<n+1>`; `toEqual("<s>")` → `"<s>_mut"`;
181
+ `toBe(true)` ↔ `toBe(false)`; `===` → `!==` on `expect()` lines
182
+ - Python: `assert <e> == <n>` → `<n+1>`; `assert <e> != <n>` → `==`;
183
+ `assert <name>` → `assert not <name>`
184
+
185
+ A mutation the test suite still passes is **SURVIVED** — the assertion doesn't
186
+ cover the bug — and the command exits 1. All killed → exit 0. Inconclusive
187
+ runs (command missing, timeout) are reported separately and never count as
188
+ survived. Weird input fails open with a warning.
189
+
190
+ ### Post-exec read-back verifier
191
+
192
+ `install` registers `agent-guard hook-verify` as a `PostToolUse` hook on
193
+ `Bash`. After a command that claimed an outbound effect, it reads back world
194
+ state and warns on stderr (always exit 0 — it never blocks):
195
+
196
+ - `git push <remote> <ref>`: runs `git ls-remote <remote> <ref>` (10s
197
+ timeout); warns when the ref is absent remotely. `<remote>` defaults to
198
+ `origin`; `-C <dir>` is honored; unreachable remotes stay silent.
199
+ - `npm publish`: reads `name`/`version` from `package.json` and runs
200
+ `npm view <name>@<version> version`; warns when the version isn't visible.
201
+ Silent when there is no `package.json` or npm is missing.
202
+
203
+ Warnings are audit-logged (`guard: verify`, `decision: warn`).
204
+
205
+ ## Install
206
+
207
+ ```bash
208
+ # Not on PyPI yet -- install from source:
209
+ git clone https://github.com/hahahahahahahahah6/agent-guard.git
210
+ cd agent-guard
211
+ pip install .
212
+ agent-guard install
213
+ ```
214
+
215
+ `install` merges five hook entries into `~/.claude/settings.json` (backing it
216
+ up first, never clobbering existing settings):
217
+
218
+ - `SessionStart` → `agent-guard hook-snapshot` (records digests, never blocks)
219
+ - `Stop` → `agent-guard hook-test` (blocks tests-only changes)
220
+ - `PreToolUse` on `Bash` → `agent-guard hook-outbound` (blocks denylisted actions)
221
+ - `PreToolUse` on `Write|Edit` → `agent-guard hook-commentslop` (blocks
222
+ comment slop in added text)
223
+ - `PostToolUse` on `Bash` → `agent-guard hook-verify` (warns when a claimed
224
+ effect isn't visible; never blocks)
225
+
226
+ Restart Claude Code afterwards. No MCP server, no daemon, no accounts.
227
+
228
+ ## What the agent sees
229
+
230
+ Test tampering blocked at stop:
231
+
232
+ ```
233
+ Test-tampering guard: test files changed but no source files changed since
234
+ this session started.
235
+ Changed test files:
236
+ - tests/test_auth.py
237
+ This matches a known failure mode where an agent edits test assertions to make
238
+ them pass instead of fixing the source code. Before proceeding, verify each
239
+ changed test actually fails without the fix: revert the source change, re-run
240
+ the test, and confirm it goes red. A test that stays green without the fix is
241
+ not covering the bug.
242
+ ```
243
+
244
+ Outbound action blocked before it runs:
245
+
246
+ ```
247
+ Outbound-action guard blocked this command (rule 'git-push-protected': Push to
248
+ a protected branch (main/master/prod*/release/*): push to protected ref 'main').
249
+ If this action is intended, add an allowlist regex to outbound.allow in
250
+ ~/.config/agent-guard/config.json, or run it yourself outside the agent.
251
+ ```
252
+
253
+ Note: push blocking is destination-aware. A bare `git push` (or
254
+ `git push <remote>`) resolves the destination to the current branch via
255
+ `git symbolic-ref --short HEAD` in the hook's working directory
256
+ (`git -C <dir>` is honored), so pushing a feature branch is allowed and
257
+ only pushes whose destination is a protected branch are blocked. If the
258
+ branch can't be determined (detached HEAD, not a git repo), the push is
259
+ allowed rather than breaking your workflow.
260
+
261
+ ## Configuration
262
+
263
+ `~/.config/agent-guard/config.json` (all optional):
264
+
265
+ ```json
266
+ {
267
+ "test_guard": {
268
+ "mode": "block",
269
+ "ignore_paths": ["tests/legacy/"]
270
+ },
271
+ "outbound": {
272
+ "allow": ["my-registry\\.internal"],
273
+ "deny_extra": ["rm -rf /tmp/scratch"],
274
+ "disabled_rules": ["cloud-provision"]
275
+ },
276
+ "comment_slop": {
277
+ "mode": "block",
278
+ "threshold": 30
279
+ },
280
+ "cheat_sniff": {
281
+ "mode": "block",
282
+ "threshold": 30,
283
+ "allow": []
284
+ }
285
+ }
286
+ ```
287
+
288
+ - `test_guard.mode`: `"block"` (default) or `"warn"`. Env override:
289
+ `AGENT_GUARD_TEST_MODE=warn`.
290
+ - `comment_slop.mode`: `"block"` (default) or `"warn"`. Env override:
291
+ `AGENT_GUARD_COMMENT_MODE=warn`.
292
+ - `comment_slop.threshold`: slop score (0–100) at which the comment hook
293
+ trips. Env override: `AGENT_GUARD_COMMENT_THRESHOLD`.
294
+ - `cheat_sniff.mode`: `"block"` (default) or `"warn"`. Env override:
295
+ `AGENT_GUARD_CHEAT_MODE=warn`.
296
+ - `cheat_sniff.threshold`: cheat score (0–100) at which the cheat hook
297
+ trips (one severe hit reaches the default 30). Env override:
298
+ `AGENT_GUARD_CHEAT_THRESHOLD`.
299
+ - `cheat_sniff.allow`: `"path-or-basename:kind"` entries that suppress hits,
300
+ e.g. `"test_sort.py:rng-seed"` or `"*/legacy/*:*"`.
301
+ - `outbound.allow`: regexes that win over the denylist (e.g. your internal
302
+ registry). Every override is audit-logged.
303
+ - `outbound.deny_extra` / `disabled_rules`: extend or trim the denylist.
304
+
305
+ Inspect decisions:
306
+
307
+ ```bash
308
+ agent-guard log # blocked + allowlist-override + verify-warn decisions
309
+ agent-guard status # state paths, mode, active rules
310
+ ```
311
+
312
+ Check whether your tests are honest:
313
+
314
+ ```bash
315
+ agent-guard mutate-check tests/test_billing.py -- pytest -q
316
+ ```
317
+
318
+ ## Honest limitations
319
+
320
+ - **Heuristic, not proof.** "Tests changed, source didn't" is a strong
321
+ signal of the navune cheat, not a proof. Legitimate test-only refactors get
322
+ blocked too — that's what `ignore_paths` and warn mode are for.
323
+ - **New tests are allowed.** Added test files never count as tampering;
324
+ only modified or deleted ones do.
325
+ - **Bash only (for now).** The outbound guard watches the `Bash` tool. An
326
+ agent reaching a Slack MCP tool directly is out of scope for this MVP.
327
+ - **Best-effort spend list.** Cloud-provision patterns cover the common
328
+ CLIs; exotic spend paths won't match. The denylist is a seatbelt, not a
329
+ vault.
330
+ - **Hook protocol is undocumented.** The hook stdin shape and the
331
+ exit-2-blocks convention come from community documentation, not a stable
332
+ API. If Claude Code changes the protocol, the hooks degrade to fail-open
333
+ allow.
334
+ - **Per-machine only.** State lives in `~/.config/agent-guard/`.
335
+ - The hooks **fail open**: corrupt state, unreadable files, malformed input —
336
+ anything unexpected means "allow". A guard that wedges your session is
337
+ worse than no guard.
338
+ - **Script-content inspection is one level deep.** A script that executes
339
+ another script (`bash a.sh` where `a.sh` runs `bash b.sh`) is not followed;
340
+ exotic interpreter wrappers beyond `env`/`sudo`/`nohup`/`time`/`nice` are
341
+ not unwrapped. The denylist is a seatbelt, not a vault.
342
+ - **Mutation checking is regex-based, not semantic.** It generates at most 20
343
+ first-order mutants with simple syntactic operators — good enough to catch
344
+ vacuous assertions, not a replacement for real mutation-testing tools.
345
+ - **The post-exec verifier is advisory.** It warns on stderr and always exits
346
+ 0; unreachable remotes, missing npm, and timed-out checks stay silent
347
+ rather than crying wolf.
348
+ - **Slop detection is stylistic, not semantic.** The six patterns are regex
349
+ heuristics tuned for low false positives, which means they miss subtler
350
+ slop (a well-written but pointless paragraph scores 0). Short added
351
+ comments normalize aggressively — one narrative line in an otherwise
352
+ comment-free edit scores high, by design. If your codebase has a
353
+ comment-heavy style (or non-English comments the patterns don't cover),
354
+ use warn mode or raise `comment_slop.threshold`.
355
+ - **`decomment --fix` only removes commented-out code.** Other slop kinds
356
+ are reported, never auto-edited — deleting prose automatically is how you
357
+ lose the one comment that mattered.
358
+ - **Cheat-sniffing is static and Python-first.** It reads text, not runtime
359
+ behavior — a cheat applied only at runtime (e.g. via `sitecustomize.py`
360
+ or an installed plugin) is invisible to it. The "subject vs collaborator"
361
+ judgment is a filename heuristic (`test_billing.py` → `billing`); exotic
362
+ layouts need the allowlist. A fixed seed with a reproducibility marker is
363
+ trusted on the marker's word — an agent that writes "reproducible" next
364
+ to a planted seed fools the exemption, which is why severe kinds
365
+ (mock-subject, rng-patch, conftest-patch) have no marker exemption at all.
366
+ - **The cheat hook only watches added test text.** Pre-existing cheats in
367
+ the repo are found by `cheatsniff --check`, not blocked by the hook.
368
+
369
+ ## Roadmap
370
+
371
+ - ~~**Cheat-sniffing beyond test files**: catch RNG rigging, subject-mocking,
372
+ and conftest plants (the remdore study: half of cheating never touches
373
+ test files).~~ — shipped in v0.4 as `hook-cheatsniff` +
374
+ `cheatsniff --check`.
375
+ - ~~**Comment-slop guard**: intercept the agent dumping conversation state into
376
+ code comments (the "9 out of 10 of my revisions is deleting comments"
377
+ complaint), or a one-command decomment pass before PRs.~~ — shipped in v0.3
378
+ as `hook-commentslop` + `decomment --check/--fix`.
379
+ - ~~**Test-honesty hook**: automate the mutation idea — deliberately break an
380
+ assertion, run once, require red~~ — shipped in v0.2 as `mutate-check`.
381
+
382
+ ## Development
383
+
384
+ ```bash
385
+ python3 tests/test_agent_guard.py # 47 smoke tests
386
+ python3 tests/test_cheatsniff.py # 26 cheat-sniff tests
387
+ ```
388
+
389
+ ## License
390
+
391
+ MIT