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.
- agent_guard_hooks-0.4.0/LICENSE +21 -0
- agent_guard_hooks-0.4.0/PKG-INFO +391 -0
- agent_guard_hooks-0.4.0/README.md +378 -0
- agent_guard_hooks-0.4.0/pyproject.toml +22 -0
- agent_guard_hooks-0.4.0/setup.cfg +4 -0
- agent_guard_hooks-0.4.0/src/agent_guard/__init__.py +12 -0
- agent_guard_hooks-0.4.0/src/agent_guard/__main__.py +6 -0
- agent_guard_hooks-0.4.0/src/agent_guard/cheatsniff.py +406 -0
- agent_guard_hooks-0.4.0/src/agent_guard/cli.py +584 -0
- agent_guard_hooks-0.4.0/src/agent_guard/commentslop.py +512 -0
- agent_guard_hooks-0.4.0/src/agent_guard/mutate.py +327 -0
- agent_guard_hooks-0.4.0/src/agent_guard/outbound.py +612 -0
- agent_guard_hooks-0.4.0/src/agent_guard/state.py +164 -0
- agent_guard_hooks-0.4.0/src/agent_guard/testguard.py +173 -0
- agent_guard_hooks-0.4.0/src/agent_guard/verify.py +303 -0
- agent_guard_hooks-0.4.0/src/agent_guard_hooks.egg-info/PKG-INFO +391 -0
- agent_guard_hooks-0.4.0/src/agent_guard_hooks.egg-info/SOURCES.txt +20 -0
- agent_guard_hooks-0.4.0/src/agent_guard_hooks.egg-info/dependency_links.txt +1 -0
- agent_guard_hooks-0.4.0/src/agent_guard_hooks.egg-info/entry_points.txt +2 -0
- agent_guard_hooks-0.4.0/src/agent_guard_hooks.egg-info/top_level.txt +1 -0
- agent_guard_hooks-0.4.0/tests/test_agent_guard.py +645 -0
- agent_guard_hooks-0.4.0/tests/test_cheatsniff.py +335 -0
|
@@ -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
|