Back to Insights
22 Jun 20265 min readAI Agents

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 modeChannelGuardWhat it catches
Hidden-Unicode / invisible-char injectionina frisk lint (open source as agent-frisk)Invisible characters and smuggled instructions in the text the agent is fed
Foreign-script intrusion, mojibake, runaway repetitionout — prosea prose lintA stray CJK run, a �, or a line repeated five times in clean English
Leaked API key / tokenout — secretsa secrets lintA known-shaped secret echoed into a reply, command, or file — redacted, never reprinted
Stuck / spinning session burning tokensdoesa waste lintSame call retried, same error recurring, edits ping-ponging — work that costs money and produces nothing
False completion ("done", "tests pass", "created X")claimsa claims lintA 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.

AI agentsreliabilitylintCIagent opsopen source

Found this useful?