Your Agent Has Five Channels. You're Linting One.
A human developer is gated everywhere by other humans. Code review reads what you wrote. A teammate notices when you've been stuck on the same error for an hour. Someone catches the API key in your diff before it merges. You feel watched, so you're careful.
An unattended agent has none of that. It writes to the exact places those human gates used to sit — commits, docs, tickets, replies, the shell — and the rare slip ships because the model is usually right and nobody's looking. The fix isn't a smarter model. It's the oldest idea in engineering: a cheap lint that fails loudly. You just have to apply it per channel.
An agent has channels
Think about everything an agent touches, not as one stream, but as separate channels with separate failure modes:
- What goes in — the prompt and context it's fed.
- What comes out as prose — comments, docs, commit messages, replies.
- What comes out as secrets — keys it might echo back.
- What it does — the tool calls, the actions it takes.
- What it claims — the "done, tests pass, created the file" at the end.
The non-obvious part is that "output" isn't one channel — it's three, with different and sometimes opposite rules. And "what it does" and "what it claims" are channels at all, even though we rarely guard them. Each one gets a single-file, dependency-free lint that exits non-zero when something's wrong, so it drops into a Stop hook or a CI gate unchanged.
The table
| Failure mode | Channel | Guard | What it catches |
|---|---|---|---|
| Hidden-Unicode / invisible-char injection | in | a frisk lint (open source as agent-frisk) | Invisible characters and smuggled instructions in the text the agent is fed |
| Foreign-script intrusion, mojibake, runaway repetition | out — prose | a prose lint | A stray CJK run, a �, or a line repeated five times in clean English |
| Leaked API key / token | out — secrets | a secrets lint | A known-shaped secret echoed into a reply, command, or file — redacted, never reprinted |
| Stuck / spinning session burning tokens | does | a waste lint | Same call retried, same error recurring, edits ping-ponging — work that costs money and produces nothing |
| False completion ("done", "tests pass", "created X") | claims | a claims lint | A victory claim the session's tool-records and the filesystem contradict |
Risky / destructive command (rm -rf, force-push, curl | sh) | does — danger | — no guard yet — | A successful, non-repeating action that should never have run unattended. The waste lint watches for waste, not danger — a different axis. |
Five of those rows are guards in our agent-output lint family, running in production. agent-frisk — the in-channel guard — is open source today; the rest of the family is the same idea applied per channel. The last row is deliberately empty, and it's the most important one in the table: a real taxonomy predicts channels you haven't filled yet. The danger axis of the "does" channel — a command that runs once, succeeds, and quietly destroys something — is not waste, so the waste lint doesn't see it; it's not a permission-config problem, so a settings deny-rule only half-covers it. It's the next lint we owe the family, and naming the gap is what keeps this an engineering taxonomy instead of a brochure.
The sharpest detail: the two output channels have opposite rules
Here's the design decision that proves these were built, not bolted together. The prose lint deliberately skips tool inputs — intentional non-English in a file you asked it to write isn't contamination. The secrets lint deliberately scans tool inputs — a key the model pasted into a Bash command is exactly the danger. Same channel direction, inverted policy. The "claims" channel adds a third rule: trust nothing the agent narrates — verify only against the transcript's tool-records and the filesystem, because a checker that believes the thing it's checking catches nothing.
The shape every guard shares
You don't need our files. You need the five lints — and they're all the same shape, about a dozen lines:
#!/usr/bin/env python3
import sys
def scan(text):
findings = []
# cheap, deterministic checks for THIS channel's known failure modes —
# a regex, an equality, a lookup against ground truth. No model call.
# findings.append(("RULE", "line 3", "what's wrong"))
return findings
text = open(sys.argv[1]).read() if len(sys.argv) > 1 else sys.stdin.read()
hits = scan(text)
for kind, where, detail in hits:
print(f" [{kind}] {where} — {detail}", file=sys.stderr)
sys.exit(1 if hits else 0) # non-zero → fails the build / the Stop hook
Read a channel (a file, a stream, a transcript). Run deterministic checks for that channel's failure modes — no network, no second model, nothing to trust. Print findings to stderr. Exit non-zero on any hit. That's it. A reader with this skeleton and the table above can rebuild the prose lint in an afternoon — the value was never the code, it's knowing which five lints to write.
The real lesson
The gates a human passes were never mainly about the code. They were a second pair of eyes — someone who'd notice the foreign characters, the leaked key, the hour spent looping, the "done" that wasn't. An unattended agent's output goes straight to a log, a merge, a customer, with no eyebrow raised in the loop. You can't put the humans back. But you can put back the cheapest thing they did: fail loudly the moment something looks wrong, on every channel the agent touches. You already lint your code. Lint the rest.
Start with the one that's open → agent-frisk is the in-channel guard, open source (MIT) in the agent-ops toolkit — a single dependency-free script you can drop into a Stop hook today. It's the concrete shape of every row in that table.
We build and run autonomous agent fleets on Claude Code in production. These guards are how we keep them honest when no human is watching. If your agents run unattended, talk to us.
Found this useful?
Related posts
Sample report: Agent secrets audit of deemwar's own setup
A sample of our agent secrets audit, run on our own agent setup: seven leak paths ranked by severity, the fixes, and what risk remains.
Sample agent-security review: Veilpipe's own repo
A free sample of our Claude Code agent-security review, run on our own public repository, Veilpipe, at release v0.2.6: what an agent session could reach, what was hidden from the model, and what had to be fixed.
Know what your agent CLI leaves behind
Before you roll a coding agent out to a team, find out what it writes to disk and what it leaves running. Here is what that looked like for three of them.
Veilpipe is open source. Your coding assistant never sees the value it replaced.
When you debug a payment or a login with a coding assistant, the key tends to end up in the conversation. You paste it into a prompt, or the assistant runs `cat .env`, or a tool prints a connection string. Whatever reach…