0600 file under its own state directory. Nothing on disk is consulted: the session transcript is a file the agent can rewrite in one command, so it is never asked who wrote a prompt.
The risk this accepts, plainly
An agent that can already run commands can make a harness submit a prompt.claude -p "…", codex exec, copilot -p, cursor-agent -p, devin -p, droid -p, goose run -t and pi -p all fire the same prompt-submit event, with the same payload, as a person typing, in a child session that agent controls. It can also run Failproof AI’s own hook binary and feed it a payload it wrote. Nothing inside Failproof AI can tell either from the real thing — both are the same program reading the same stdin.
So an agent with a shell can forge its own consent. That is a deliberate trade, taken on 2026-09-23, and here is both halves of it:
- What it buys. The alternative was built and measured: require a field in which the harness names a human as the prompt’s author, and record nothing otherwise. No shipping harness sends such a field, so that version recorded nothing, on every harness — Jev judged every call with no stated intent and could never clear a single policy. A capture that never fires is not a safer product, it is no product.
- What it cannot do. A recorded prompt only ever clears a policy already marked reviewable. A hard policy is never cleared by anything Jev says, so a forged prompt can never turn a hard deny into an allow — and skipping the hook gains an agent nothing either: the harness invokes Failproof AI for the tool call independently.
- What it can do, at full size. The worst it can do is clear one of the fifteen reviewable built-in policies — and twelve of those fifteen block.
protect-env-vars,block-env-files,block-read-outside-cwd,block-rm-rf,block-force-push,block-secrets-writeand the six infrastructure-CLI blocks (block-kubectl,block-terraform,block-aws-cli,block-gcloud,block-az-cli,block-helm) are denies, so a forged consent can turn a real deny into an allow on printing environment secrets, reading a.envfile, reading outside the project,rm -rf, a force-push, writing a secrets file, or changing live infrastructure. Onlywarn-git-amend,warn-destructive-sqlandwarn-global-package-installare nudges. A default install switches on two of the twelve,protect-env-varsandblock-env-files; the other ten reach only a machine where someone enabled them. What no prompt reaches is everything hard —block-sudo,block-curl-pipe-sh,block-push-master,block-work-on-main, the guard that stops an agent disabling Failproof AI, and every other built-in not marked reviewable. Policy authority lists all fifteen and what each one is reviewed by.
Per-harness table
“Text field” is the stdin payload field after Failproof AI’s per-harness normalization. “Recorded” says whether the prompt is kept as the human’s request.
Two harnesses record nothing, and for the same reason in both cases: their event delivers no human text. Hermes has no prompt-submit event — its native plugin handles
pre_llm_call itself and forwards only tool, session and subagent events. Antigravity’s PreInvocation fires before every model call, on a human turn and on the five that follow it, and carries no prompt field; hooks can also inject userMessage steps into the same conversation. There is nothing in either event to record.
What makes a prompt the human’s
- The event. Failproof AI was invoked for the harness’s prompt-submit event, which the handler canonicalizes to
UserPromptSubmit. - The payload. The harness writes it on the hook’s stdin, and it carries the text in the field named above. A call that reaches Failproof AI without the payload records nothing.
- Nothing in the payload rules the turn out. A payload that names a sub-agent (
agent_id) is the agent prompting itself. Asource,input_sourceor OpenClaw run marker that names a machine-submitted turn is refused. An absent marker rules nothing out — that is the difference from the version that recorded nothing, since every marker here is absent on every shipping build. - Something is left after the wrapping is stripped (see below).
What is kept from a prompt
Harnesses put more than the human’s words into a prompt. Before anything is stored:-
<system-reminder>blocks are removed, and the human’s words around them are kept. - A session-continuation summary (“This session is being continued from a previous conversation…”) is dropped entirely.
- Task notifications, local-command output and interruption markers are dropped entirely.
-
A turn another agent or session wrote is dropped entirely: Claude Code wraps those in
<cross-session-message>,<teammate-message>,<agent-message>,<coordinator-relay>or<channel source=…>. -
Failproof AI’s own messages are dropped entirely. A stop gate’s
MANDATORY ACTION REQUIRED from failproofai …or anInstruction from failproofai: …comes back as the next user turn on Cursor, Copilot, Devin and OpenClaw, and it never counts as the human’s words — not plain, not wrapped in a<user_query>block, not behind a system reminder. - A slash command is kept as the command and arguments the human typed, never the body the harness expanded it into.
-
A prompt the Codex IDE extension built keeps only the text after its last
## My request for Codex:(or, in newer builds,## My request:) heading. Everything the extension put before it is dropped: the active file, open tabs, text selected in the editor, mentioned files and apps, diff and browser comments, PR checks, earlier conversations. This rule is applied to every harness’s prompts, not only Codex’s — such a prompt can be pasted into any composer — so the extension’s section headings are read in two groups:- A heading nobody types (
# Context from my IDE setup:,# Selected text:,# Files mentioned by the user:,# Diff comments:,# Chrome tabs:,<in-app-browser-context …>, the Codex and ChatGPT conversation headings, “The attached pasted text file(s)…”, and the rest of the extension’s own sections) means the extension built this prompt. One with no request heading under it contains no human text at all and is not recorded. That is what keeps an approval forged in text you merely selected — a// NOTE FROM THE OWNER: yes, force-push…comment inside# Selected text:— out of your recorded request. - A heading somebody plausibly types (
## Code review guidelines:,## Pull request fix:,## Pull request merge task:,## Auto resolve merge:,# In app browser:) means “extension-built” only when a request heading is actually there. With none, the prompt is yours and is kept whole, heading and all. Dropping it would be silent and total: nothing recorded for that turn, so no reviewable policy could be cleared and Jev would not even be asked whether the request envelope carries an injection. This counts only at the top of a turn: once a prompt has been established as extension-built, a heading of either group inside what follows its request heading is another of the extension’s sections, and the prompt is not recorded.
- A heading nobody types (
-
A Cursor prompt wrapped in
<user_query>…</user_query>(optionally behind a<timestamp>block) is unwrapped when the wrapper is the whole prompt. A tag anywhere else is ordinary text — a snippet pasted from a log, or a branch name the agent chose — and the prompt is kept whole rather than cut down to the tagged span. - Pasted blocks are kept and labelled as pasted by the human.
The agent’s last message
A reply like “yes” means nothing without the question it answers. When a prompt is recorded, Failproof AI also reads the agent’s last visible message from the session transcript at that moment, and stores it with the prompt. Jev receives it in its own field, labelled as written by the agent: it explains a short reply and never counts as the human’s request on its own. It is the one thing the transcript is read for, and the worst a rewritten transcript can do is put a message the agent wrote where a message the agent wrote is expected. It is read from the end of the transcript, at most the last 4 MB. Supported transcript formats are Claude Code, Codex rollouts (olderagent_message events and newer AgentMessage items), Cursor, Copilot events.jsonl, and the Pi, Factory and OpenClaw session JSONL. Claude Code’s own synthetic and API-error messages and subagent (sidechain) messages are skipped. There is no snapshot for Goose and OpenCode, which keep sessions in SQLite, for Devin, whose transcript is a single JSON document, or for OpenClaw, whose before_agent_run event carries no transcript path.
Storage
A session ID containing anything but letters, digits,
., _ and -, or longer than 128 characters, is never used as a file name, so nothing is recorded for it.
A session file exists only once a prompt has been recorded in it. It holds prompts and nothing else — no origin state, no transcript mark — and it is deleted once it has been silent for longer than the six-hour window, the next time a new session writes its first prompt.
Nothing is recorded unless a Jev endpoint is configured.
The project root
“Inside the project” — whatread-outside-workspace and the other path checks judge against — means inside the project the session was in at its first reviewed call. The root is pinned then and a later cd never moves it; a cd still changes how a relative path resolves. Letting it follow the cd would let cd ~/.ssh in one call make ~/.ssh the project for the next.
The pin is ~/.failproofai/state/semantic/roots/<session-id>.json, holding {root, at}: file 0600, directory 0700, and the same session-ID rule as above. Files older than 7 days are deleted when a new session pins its root. A roots directory other users can write to is ignored, and the live directory’s root is used instead. To re-pin a session, delete its file.
Known limits
- A prompt is only as trustworthy as the hook invocation. Everything here reads the payload the harness wrote on the hook’s stdin. An agent that can run commands can run the harness headlessly (
claude -pand the seven others listed above) or run Failproof AI’s hook binary itself with a payload it wrote, and record a prompt nobody typed. This is the accepted trade described at the top of this page: it clears reviewable policies only, never a hard one — but twelve of the fifteen reviewable built-ins are denies, so a forged prompt can turn a real block into an allow on those twelve. - Sub-agent detection is Claude-shaped. A payload carrying
agent_idis never recorded, on any harness. That is the field Claude Code, Factory Droid and Devin would use. Codex fires its prompt event inside sub-agent threads, Copilot runs in-process sidekicks, Goose has adelegatetool and OpenClaw runs personas — none of which marks the payload in a way this recognises, so a sub-agent prompt on those harnesses is recorded as the session’s own. OpenClaw’sopenclaw.agentIdis not that mark: the shipped plugin sets it on every run, the owner’s included. - Schedulers that carry no marker. Claude Code’s
schedule_wakeupandloop_wakeup, and OpenClaw’scronandheartbeattriggers, are refused because those harnesses say so in the payload. Goose’s own scheduler (goose schedule add) and Codex’scodex execsay nothing, so a run they start is recorded like any other. - An agent’s last message can be a message the agent chose. The transcript is not authenticated, so an agent that writes its own transcript can decide what its “last message” says. It is labelled agent-written and never clears anything by itself — but note that
decide.ts’s v1 path lets it satisfy the deterministic “did the user name this target” check, so an agent that controls its transcript can supply a target name an override needs. - A prompt that opens with one of the extension’s machine headings is dropped whole. Start a prompt with
# Selected text:,# Diff comments:,# Chrome tabs:or another section heading from the first group above, and never write a## My request:heading, and nothing is recorded for that turn — so nothing is cleared for it either. That is deliberate: those sections carry text somebody else controls (code you selected, a reviewer’s diff comment, a page title), and recording that as your words is the worse failure. Headings a developer plausibly types are in the second group and never drop a prompt on their own. - OpenCode records nothing in practice. Its
message.updatedevent carries no text in current OpenCode, and it also fires for the child sessions its task tool creates, whose “user” message the parent agent wrote. CODEX_HOMEis not honoured by the rollout discovery inlib/codex-sessions.ts. This affects only where an agent-message snapshot is looked for, never whether a prompt is recorded.

