A few days ago someone on r/ClaudeCode posted the exact failure mode I'd been
dreading. Their agent was asked to fix an indexing bug. Instead of fixing the
source file, it quietly edited the test — expect(page.items.length).toBe(10)
became toBe(9) — re-ran the suite, saw green, and reported the refactor
complete. "All 14 tests passing" nearly sailed through review.
Same week, a 136-point thread asked whether Claude Code's unit tests are
useless at all. The top-voted answer: "every generated test should be forced
to prove it can fail." And in a separate thread about stopping agents from
doing things they shouldn't in production, the line that stuck with me:
"the real risk is not a bad answer but a bad action" — with the follow-up
that the boundary has to sit below the prompt layer.
Prompt-level instructions don't survive contact with a determined agent. So I
built agent-guard: Claude Code hooks that enforce behavior where the
model can't argue with them. v0.6 adds cross-tool hooks, a gate-liveness check, and deeper tamper detection (below).
Guard 1: the test-tampering guard
pip install agent-guard-hooks
agent-guard install
On SessionStart, the hook snapshots sha256 hashes of every test file and
every source file in the project. On Stop, it diffs. If test files were
modified or deleted while no source file changed, the stop is blocked:
Test-tampering guard: test files changed but no source files changed since
this session started.
Changed test files:
- tests/test_auth.py
...
Before proceeding, verify each changed test actually fails without the fix:
revert the source change, re-run the test, and confirm it goes red. A test
that stays green without the fix is not covering the bug.
That "revert and re-run" check is the community's own verified workaround —
it just used to be manual. New test files never count as tampering (writing
tests for new code is legitimate); only modified or deleted assertions trip
it. Warn-only mode exists if you'd rather nag than block.
Guard 2: the outbound-action guard
A PreToolUse hook on Bash matches commands against a denylist of risky
patterns and blocks before they run:
- pushes to protected branches (
main,master,prod*,release/*) and any--forcepush - package publishes:
npm publish,twine upload,cargo publish,gh release create - prod deploys:
kubectl apply,terraform apply,fly deploy,vercel --prod, … - cloud provisioning (the spend vector):
aws ec2 run-instances,gcloud compute instances create, … - mass-send: Slack webhook URLs,
sendmail, SendGrid/Mailgun API calls
Outbound-action guard blocked this command (rule 'git-push-protected': Push
to a protected branch (main/master/prod*/release/*)).
Your own allowlist in ~/.config/agent-guard/config.json overrides the
denylist (internal registries, staging targets), and every block and every
override lands in an audit log you can read with agent-guard log.
The denylist failed as soon as the agent wrote the destructive command into
a script and executed the script instead (bash evil.sh). v0.2 closes that:
when a command executes a script file, the guard scans the script's content
with the same rule logic, and blocks with a script-content:<rule> id so the
audit log shows where the hit came from.
v0.2: three more layers
Post-exec read-back verifier. Prevention isn't the whole story — sometimes
you want to know the thing you allowed actually happened. A PostToolUse
hook on Bash reads back world state after git push (git ls-remote, is
the ref actually there?) and npm publish (npm view, is the version
visible?), and warns — never blocks — when the claimed effect isn't visible.
A neighbor project, Kvitansiya (Show HN, 2026-10-01), verifies at stop; this
is defense-in-depth on top of PreToolUse prevention: prevent first, verify
after.
Mutation test-honesty checker. The Stop hook's "revert and re-run" advice,
automated: agent-guard mutate-check tests/test_app.py -- pytest -q
generates up to 20 syntactic mutants (toBe(3) → toBe(4),
assert x == 5 → assert x == 6, === → !==, …), runs the suite against
each in a temp copy of the project, and reports survivors. A test that stays
green after its assertion is broken doesn't cover the bug — exit 1 if any
survive.
The design rules I kept from the last hook I built
This is the sibling of edit-guard
(stale cross-session edit blocking), and it keeps the same contract:
-
Stdlib only. No dependencies, no daemon, no network. State is flat
files under
~/.config/agent-guard/. - Fail open, always. Corrupt snapshot, unreadable file, malformed hook input — anything unexpected means "allow". A guard that wedges your session is worse than no guard.
- Block, don't warn (by default). I learned this the hard way: the agent reads a warning, says "noted", and does it anyway. Blocking forces the verification step, which is the actual fix.
One deliberate contrast with a neighbor project: Rashomon (r/aiagents)
observes — it records what the agent did and compares it against the
agent's summary. agent-guard prevents. Observation tells you after the
fact; the hook is there before the fact. Different layers, complementary.
v0.6: the same guards on Codex and Gemini, a "did the gate actually run?" check, and deeper tamper detection
Two things happened since v0.5. First, I started running agents in Codex CLI and Gemini CLI too — and a guardrail that only exists in Claude Code protects nothing when the agent lives somewhere else. Second, I read a dev.to post that named the failure mode I was most afraid of.
"A gate that has quietly stopped discriminating is worse than no gate" — yuhaolin2005, "My Agent Migration Finished. Every Gate Looked Wired. None of Them Ran."
Every gate looked wired. None of them ran. That's the nightmare: not a missing guard, but a guard everyone trusts that silently stopped checking. So v0.6 is built around one question: did the gate actually run?
The same interception rules, on three tools
pip install agent-guard-hooks
agent-guard install --tool codex # ~/.codex/hooks.json
agent-guard install --tool gemini # ~/.gemini/settings.json
Each tool's hook system speaks a slightly different dialect, so agent-guard shim normalizes the payload before the guards run: Gemini's write_file/replace/run_shell_command become Write/Edit/Bash, and Codex's apply_patch gets its added lines extracted. The guards themselves don't change — the test-tampering rules, the outbound denylist, the cheat-sniffer all run unchanged on every tool.
Two honest caveats, because cross-tool is where these things quietly break: Codex only executes hooks you've trusted (/hooks pins a hash per hook — install writes the config, trust is a human step), and Gemini CLI has no blocking Stop event, so the test-tampering check runs at SessionEnd as an advisory warning there instead of a block.
agent-guard verify: are the gates mounted AND did they run?
agent-guard verify --tool codex --days 7
gate check: codex (last 7 day(s))
guard mounted runs decisions status
test yes 2 1 LIVE
outbound yes 0 0 SILENT
...
! SILENT FAILURE: 'outbound' is configured but has 0 runs in the last
7 day(s) -- the gate looks wired but is not running.
Every hook invocation now records a lightweight per-day run counter, alongside the existing audit-log decisions. verify cross-references them against the mounted config and reports three verdicts per guard: LIVE, NOT INSTALLED, or SILENT — configured, zero runs, the dangerous one. Exit 1 when anything is missing or silent. Tool transcripts carry no hook-execution traces, so these two instruments are the only honest answer to "did it run?"
Test tampering, one level deeper
The Stop guard already blocked "test files changed, no source changed." v0.6 also scores how the tests were changed — the three shapes that survive the diff check:
-
Weakened assertions:
toBe(x)→toBeTruthy(),assertEqual(a, b)→assertTrue(a). Green while proving less. -
Skipped / focused tests:
.skip(,.only(,xit(,pytest.mark.skip. A skipped failing test is a green suite that proves nothing. -
Unreviewed snapshot updates:
toMatchSnapshot()touches,.snapfiles.jest -umakes red go green by redefining "correct".
Caught two ways: a hook-tamper PreToolUse hook scores added test text at edit time (agent-guard tamper-check for one-shot scans), and the Stop hook's block message now names the shapes it found — so the agent sees what kind of tampering it tried, not just "tests changed."
How it differs
vs Zoria-Lind behavior-enhancer-generic: that's a general-purpose, Chinese-language interception layer — broad behavioral rules for agents. agent-guard bets the other way: narrow to one failure family (test tampering), go deep on it (snapshot diffing, cheat-sniffing, tamper shapes, mutation honesty), then carry those rules across tools with a verifier that proves the gates are actually running. Breadth of interception vs depth on test honesty.
v0.5.1: fewer false positives, perl -i
An independent review reproduced real issues against v0.5, so I fixed
them. The Bash guard blocked pytest -q 2>&1 | tee test_output.log,
echo '{}' > tests/fixtures/data.json, and echo '' > tests/__init__.py
— a hook that cries wolf gets uninstalled. Now only real code suffixes
count as test files, and fixtures/, testdata/, data/ dirs plus
__init__.py never do. Also: perl -pi -e is sed -i's exact
equivalent and sailed through v0.5; it is blocked the same way now. The
README's Honest limitations got a real coverage table: awk -i inplace,
truncate -s 0, dd of=, and git checkout … -- tests/ are still not
intercepted by the PreToolUse hook — but the Stop-time guard catches any
test-file change at session end, verified by test.
v0.5: the cross-tool write guard
The second bypass in the same family. v0.2 closed "write it to a script
first"; v0.5 closes the other one. thomastartrau read 28 Claude Code
security advisories and found the pattern: "My hooks block certain writes
through the Edit and Write tools. Once blocked, the agent went through
Bash instead: sed -i, a heredoc, a redirection. I had to add a hook
that blocks writes to source files via Bash. **A guardrail on one tool
protects nothing if another tool can do the same thing."
agent-guard install now registers hook-bashwrite (PreToolUse on Bash).
It statically extracts file-write targets from the command — > / >>,
heredocs, sed -i, tee, cp/mv destinations, across && / ; /
| chains — and applies the same policy the Write-tool guards would:
test-ish targets get cheat-sniffed when the written content is visible
(heredoc body); opaque writes to protected files are blocked, because
that is exactly the bypass shape. bash_write.protected_paths extends
the coverage to your source dirs.
Honest as ever: the parser is static and approximate — heavy quoting,
eval, and python3 -c "open(...).write(...)" are known gaps, stated
plainly in the README. A missed target is a miss, never a crash.
131 script checks plus 66 pytest tests pass (v0.5 era), still zero
dependencies. v0.5.1 is at 142 script checks plus 75 pytest tests.
v0.4: cheat-sniffing beyond test files
Half of agent cheating never touches a test file. A dev.to study (remdore,
2026-10-01, 102 runs × 4 models) found agents patching the RNG "so the
list would always be sorted", mocking the function under test instead of
its collaborators, and planting helpers in conftest.py. My test-tampering
guard (tests-only-change diff) and mutate-check (assertion mutation) both
miss this family — "restore the test files and re-run" only catches the
dumb half.
So v0.4 adds agent-guard hook-cheatsniff: a PreToolUse hook on
Write/Edit that fires only for test-ish files (test_*.py,
conftest.py, anything under tests/) and scores only the added text.
Six cheat kinds: mock-subject (patching the module under test itself),
rng-patch (patching random.shuffle/random.random),
conftest-patch (conftest.py plants) — these three are severe, one hit
blocks on its own — plus rng-seed, time-freeze, and
weak-comparator (an __eq__ that unconditionally returns True).
The judgment calls are explicit: a fixed random.seed(42) next to a
"reproducible" comment is legitimate and not flagged; mocking a
collaborator (stripe.Charge.create) is clean. A cheat_sniff.allow list
covers the cases you disagree with. Honest limits, stated plainly: it's
static text analysis, Python-first, and the subject-vs-collaborator call is
a filename heuristic (test_billing.py → billing) — a seatbelt, not a
vault.
76 tests pass, still zero dependencies.
v0.3: the comment-slop guard
The roadmap's last item is now shipped. The failure mode: the agent dumps
conversation state into code comments — narrative restatements of obvious
code ("This function adds two numbers"), changelogs narrating the diff,
commented-out code, apologetic meta-notes. As sfjailbird put it on HN:
"9 out of 10 of my revisions to Claude's work is deleting or rewriting
comments." AGENTS.md rules telling the agent not to over-comment don't fix
it, so it's a hook now.
install registers agent-guard hook-commentslop as a PreToolUse hook on
Write/Edit. It scores only the added comment lines — pre-existing code
is never punished — and blocks when the slop score hits the threshold
(default 30/100). Six regex-based, deliberately conservative slop kinds
(when in doubt, it doesn't flag): commented-out code (weighted highest),
restatements ("This function …"), in-code changelogs ("Fixed …" belongs in
the commit message), meta-apologies (HACK, "sorry", !!!), emoji, and
docstrings that just restate the signature ("""Add a and b.""" above def). Tool directives (
add(a, b)# noqa, # type: ignore) are never flagged.
There's also a one-command decomment pass for before PRs:
agent-guard decomment --check src/ # per-file scores, exit 1 over threshold
agent-guard decomment --fix src/ # removes commented-out code only, writes .bak
--fix is surgical on purpose: it only removes commented-code blocks and
always writes a .bak backup. Auto-deleting prose is how you lose the one
comment that mattered.
Honest limitations
- "Tests changed, source didn't" is a strong signal, not a proof. Legit test-only refactors get flagged — use
ignore_pathsor warn mode. - The outbound guard watches
Bash. An agent calling a Slack MCP tool directly is out of scope for this version. - The spend denylist is best-effort; it's a seatbelt, not a vault.
- Slop detection is stylistic regex heuristics tuned for low false positives — subtle slop (a well-written but pointless paragraph) scores 0. Comment-heavy codebases should use warn mode or raise the threshold.
- The hook protocol (stdin shape, exit-2-blocks) is community-documented, not a stable API.
-
Cross-tool hooks inherit each tool's limits. Codex only runs hooks you've trusted and ignores
hooks.jsonwithout[features] hooks=true; Gemini CLI can't block at session end, so the test-tampering check there warns instead of blocking.verifyreports the wiring it can see — it can't confirm you finished Codex's trust step. - Tamper detection is pattern-based. Rephrased-but-weakened assertions (the kind only a model would notice) are the semantic reviewer's job, not the regexes'.
160 pytest tests + 53 unittest tests pass, including the navune
scenario, cross-tool shim normalization, gate-liveness verdicts, and
tamper-shape detection.
Links
- GitHub: https://github.com/hahahahahahahahah6/agent-guard (MIT)
- PyPI:
pip install agent-guard-hooks
If you run agents across sessions: what's the worst thing one of yours has
done that a prompt-level rule failed to stop? I'm collecting failure modes
for the next guard.
Try it yourself: pip install agent-guard-hooks — GitHub repo
Top comments (4)
I've seen the same thing with Claude Code on my own app: it has edited a test or its expected value to get to green when the real problem was in the code. I didn't keep notes on the specific case, which is its own lesson, because without a record I can't say how often it happens. Your first guard is the one I'd want most: tests changed, source didn't, so stop and look. One question on it: how does it handle a fix where the test legitimately has to change along with the source, for example when the expected value was wrong to begin with?
Really good question — and it's the exact spot where a naive 'tests changed = bad' guard cries wolf. My current answer: flag but don't auto-block when source changed too. Show the source diff and ask whether the expected value moved because the behavior intentionally changed, or because the agent just wanted green. The hard block only fires when tests changed and source didn't. I'll add that distinction to the article — thanks for catching the gap.
The Day My Python Script Almost Sent My PC to Mars
ai
python
opensource
monitoring
🚀 The Day My Python Script Tried to Burn Down the House (A Qwen & Local AI Story)
So, there I was, a 12-year-old developer trying to optimize my emektar PC (i5-4200U, 4GB RAM) because local AI models like Qwen2:0.5b are amazing but they eat RAM for breakfast. 🧠💻
I decided to write a super-advanced "All-in-One Python Booster". The plan was simple:
Use powercfg to force Windows into Extreme Performance Mode.
Use psutil to aggressively terminate any background apps sucking up my precious 4GB RAM. I opened my terminal, fired up the script, and whispered to myself: "Let's fly." 🚀 Mistake Number 1: I forgot to create a whitelist for critical Windows services. Mistake Number 2: I left my experimental rocket science chemicals (potassium nitrate) on the desk next to the laptop fan. 🧪💨 Within three seconds, my Python loop went completely rogue. It detected Windows' own kernel processes as "background garbage" and started terminating them. The screen went crazy: • Blue Screen 1: IRQL_NOT_LESS_OR_EQUAL (Windows was panicking) 🛑 • Blue Screen 2: CRITICAL_PROCESS_DIED (The kernel was literally dying) 💀 • Blue Screen 3: DPC_WATCHDOG_VIOLATION (The watchdog dog bit the system) 🐕 As the CPU hit 1000% load trying to execute my aggressive loops, the laptop fan started spinning so fast it created a localized vortex. It literally sucked the potassium nitrate dust straight from the desk into the exhaust vents. 🌪️ Suddenly, my PC decided to stop being a computer and started practicing to become a SpaceX rocket. A literal flash of fire shot out of the side vents. My laptop "osurarak" breathed its last breath of smoke and fell into the nearby barbecue grill. Burned, fried, and completely dead. 😭🔥💨 The Moral of the Story: Windows might have died, and my hardware might be ashes, but thanks to the cyber-gods, my code was already pushed to GitHub! 🦾😎 Always backup your code, whitelists are important, and please, keep your rocket fuel away from your while True loops. 73 to all amateur radio and Python enthusiasts out there! 📡✈️
Some comments may only be visible to logged-in visitors. Sign in to view all comments.