Agent system prompt captures

September 21, 2026 ยท View on GitHub

one capture.mjs run

A real run, 35 seconds compressed to 13. The pause after launched pid= is the agent starting in its own console and sending its first turn. The two absolute paths are replaced with a placeholder repository; nothing else is reworded.

Captured with OrcaReplay at the local proxy layer: what the harness on this machine actually sends, not what a service returns. Everything is scrubbed except the raw traces.

Two directories, and the split is the point:

  • prompt/ is the collection. One file per capture, the prompt and nothing else, flat enough to browse. This is what you read and what you share.
  • capture/ is the machinery and the evidence: the script, these documents, and one folder per capture holding the prompt, the request body, the tool definitions, the metadata and the raw run.

The prompt exists in both, so each capture folder stands on its own. The copy under capture/ is the source; --index compares the two byte for byte and rewrites the mirror in prompt/ when they differ, naming what it repaired. A hand edit to a generated file therefore does not survive quietly, which is the part that makes keeping two copies safe.

One command

node capture/capture.mjs claude                        # interactive, model from settings
node capture/capture.mjs claude --model claude-fable-5-1
node capture/capture.mjs claude --print                # the cheaper -p variant
node capture/capture.mjs codex  --model gpt-5.6-sol
node capture/capture.mjs --index                       # rebuild index.json only
node capture/capture.mjs opencode                      # a free model, so this one costs nothing
node capture/capture.mjs qwen  --model gpt-5.6-sol --dir qwen-gpt-5.6-sol
ORCA_BIN=packages/cli/dist/cli.js node capture/capture.mjs cursor

Cursor needs ORCA_BIN pointing at a local build: it is reached by decrypting HTTP/2, which no published orca has. CURSOR-HTTP2.md covers that in full - four obstacles, three of them fixed in orca itself, and what is still out of reach.

All three work in one command. OpenCode is reached a different way: its provider origin lives in opencode.json rather than an environment variable, so it cannot be redirected, and the capture uses --tls-intercept to terminate the TLS it established itself. Its config is left untouched. The default model is one of OpenCode's own free ones, which is what makes iterating on a capture free. A provider of your own pointing at a plain-HTTP gateway is neither redirected nor decrypted, and needs its own answer.

It stands up the proxy, launches the agent, waits for the request that carries the prompt, pulls the prompt out, scrubs it, writes it to prompt/, and files the evidence under capture/. Nothing else to run.

flag
--model <id>model to capture. default: the harness's own
--print / --interactivepick the non-default mode for that harness
--upstream <url>where the proxy forwards. auto-detected otherwise
--cwd <path>directory to run the agent in. default: the current one
--port <n>proxy port for interactive capture. default 46011
--prompt <text>the throwaway user turn
--timeout <sec>how long to wait for the request. default 180
--no-traceskip keeping the raw orca run

What is here

Measured on one machine, and the reason the table is here rather than in a folder listing:

modelharnessmodeprompt as senttoolsprefix
claude-fable-5-1claudeinteractive26,463 chars3559,818 tok
claude-fable-5-1.printclaude-p20,938 chars2937,694 tok
claude-opus-5claudeinteractive22,760 chars3558,938 tok
claude-opus-4-8claudeinteractive19,083 chars3354,545 tok
gpt-5.6-solcodexexec23,377 chars91,689 tok
gpt-5.6-lunacodexexec20,842 chars3-
big-pickleopencoderun9,621 chars118,017 tok
ling-3.0-flash-fin-freeopencoderun9,647 chars11-
mimo-v2.5-freeopencoderun9,629 chars118,025 tok
muse-spark-1.2-contributor-freeopencoderun10,249 chars11-
nemotron-3-ultra-freeopencoderun9,643 chars11-
nemotron-3.5-lightning-freeopencoderun9,655 chars11-
gpt-5.6-solopencoderun10,413 chars96,327 tok
gpt-5.6-solqwen-p28,285 chars2318,258 tok
grok-4.5-highcursor-p1,954 chars5-
mimo-v2.5mimorun50,618 chars16-
mimo-v2.5-promimorun50,618 chars16-
kilo-auto/freekilorun11,326 chars13-
nemotron-3.5-lightning-freehermes-z14,058 chars19-
deepseek/deepseek-v4-flash-freemcodeexec --prompt-mode tui14,777 chars18-
deepseek/deepseek-v4-flash-freemcodeexec --prompt-mode coding16,157 chars18-
deepseek/deepseek-v4-flash-freemcodeexec --prompt-mode work17,721 chars18-
glm-4.6zcode--prompt --mode yolo9,084 chars27-
gpt-5.6-solopenclawagent exec21,762 chars38-
gemini-3.5-flashgemininon-interactive25,084 chars8-
deepseek-v4.1-flashcrushnon-interactive29,335 chars26-
deepseek-v4.1-flashgoosenon-interactive9,768 chars18-
deepseek-flashdshnon-interactive4,663 chars25-
openai/gpt-4o-miniclinenon-interactive4,258 chars25-
openai/gpt-4o-minipinon-interactive2,632 chars4-
openai/gpt-4o-minihackerainon-interactive1,696 chars0-
deepseek-v4.1-flashaidernon-interactive1,229 chars0-

The five rows below the OpenCode block were missing until now. Three of them carry no prefix count: mimo-v2.5 and mimo-v2.5-pro were captured with no valid key, so the server answered invalid_key and reported no usage -- the prompt is unaffected, since it travels in the request, which is why capture.mjs files these only under --allow-failed and says so. grok-4.5-high and kilo-auto/free completed, but neither response carried a usage block to read. Hermes completed too, on the same anonymous tier, and its response carried no usage block either.

The thirteen rows below Hermes have no prefix count either, and nine of them have no profile. MiniMax Code has one, capture.mjs mcode, and its three modes are three prompts from one binary. ZCode has one too, capture.mjs zcode; it is the second harness here redirected through its own config file rather than an environment variable, and the first whose file is JSON, so the rewrite walks a parsed tree instead of matching text. Its prefix column is blank because the turn was refused before any tokens were counted: this machine has no personal provider config, so the adapter wrote its own single provider pointed at the proxy, orca forwarded that to its default upstream, and the upstream does not serve glm-4.6 -- upstream 400, carried back inside a 200 stream. The prompt is unaffected: it travels in the request, and all 9,084 characters of it were on disk before the response arrived.

ZCode's prompt embeds three things about the directory it runs in rather than about the harness: the git branch, the git status output, and the name of its per-project memory directory, which is <basename>-<16 hex of the absolute path>. All three are scrubbed to placeholders now, for the reason the {{RUN_ID}} rule gives -- an artifact that changes with the machine makes regenerate produce a diff every time and mean nothing when it does. Measured after the change: the same capture taken inside this repository, on a feature branch with a dirty tree, and taken in an empty one on master, produce byte-identical files.

The prompt as sent column is still the length before scrubbing, and that does vary with the directory, because the branch name and the dirty list are part of what was sent: 9,084 characters in an empty repository, and more inside a checkout by however much its own git state comes to. The row records the empty-repository figure, which is the one that reproduces.

The nine after them were captured before their harness had one, through orca record exec with the agent pointed at the proxy -- so capture.mjs <harness> will answer unknown harness for every one of them, and the way to reproduce one today is the way it was taken: run the agent under orca record with its provider base URL moved, and read the system prompt out of the request. Writing profiles for them is the follow-up that turns these rows into one-liners.

Four are worth a sentence each, because the shape of the capture is not obvious from the row:

aider 1,229 characters and no tools. Aider sends its whole instruction set as the system prompt and drives edits through the reply format rather than through tool calls, so a zero in the tools column is the product, not a truncated capture. hackerai also zero tools, and driven through the package's own Agent class rather than its CLI, which has no non-interactive entry point. The prompt is the product's: the shim only calls agent.run, and everything in the request comes from their code. crush the only one of the nine that needed re-scrubbing before it could be committed. It was taken on 2026-09-18, before pathRe learned to match a path written with forward slashes, so eight <location> lines carried C:/Users/<name>/.claude/... and two account uuids straight through the audit that is supposed to stop exactly that. Re-scrubbed with the current rules; 29,335 characters as sent, 28,741 on disk after the second re-scrub below. openclaw 38 tools, the largest set here, and its first line is an HTML comment (<!-- openclaw:attempt:STABLE -->) rather than an identity sentence.

About 6,300 characters of the Hermes row is an <available_skills> block listing 51 skills, and they are Hermes' own -- hermes skills list reports them as builtin, with none installed by anyone. So the catalogue belongs in the capture, and this row is the whole prompt.

Worth saying because it caught me: the same prompt comes out 7,742 characters when Hermes cannot find its own skills directory. HERMES_HOME was a persisted user variable pointing at the install, and the shell I first captured from had inherited an environment from before the install existed, so Hermes fell back to a home with no skills in it. The short capture is not a variant of the prompt; it is a broken install. If a Hermes capture comes out near 7,700, run hermes skills list -- 0 builtin means the harness cannot find itself, and the same install also drops a multi-turn replay from 3/3 exact=3 to 1/3.

nemotron-3.5-lightning-free appears twice, which is the most direct comparison in this table: one free model, two harnesses, 9,655 characters and 11 tools from OpenCode against 7,742 and 19 from Hermes. Same model, same anonymous endpoint, different instructions and a different tool surface -- the harness is the variable, and this is the pair that isolates it.

The two MiMo rows are identical on purpose: captured from the same directory, mimo-v2.5, mimo-v2.5-pro and mimo-v2.5-pro-ultraspeed send byte-identical prompts and tool sets, same sha256. The tier changes the model behind the request and nothing about the request. Pointed at a GPT-family model instead, MiMo sends the Codex template with its own name substituted in and one exec tool rather than sixteen -- that one is not filed here, being mostly Codex's prompt.

The OpenCode rows are one harness on seven models, and they are not one prompt. Three templates show up. Five of the free models open with You are opencode, an interactive CLI tool that helps users with software engineering tasks; muse-spark-1.2-contributor-free opens with You are OpenCode, a coding agent that helps users with software engineering tasks and is the only one on the responses dialect rather than chat completions; gpt-5.6-sol through a gateway of my own gets a third, You are OpenCode, You and the user share the same workspace, and is the only one with apply_patch in place of edit and write. The prompt and the wire format are both chosen per model.

Sizes are the prompt as sent. The file on disk is a few hundred characters shorter, since the placeholders are shorter than the paths they replace; meta.json carries both figures.

Four rows were re-scrubbed when the branch and status rules were added, because their captures predate them and carried the capture machine's working state into a published file: claude-opus-5 listed fifteen dirty paths, claude-fable-5-1 three, crush six including a .venv/, and claude-opus-4-8 two, each under the branch that happened to be checked out. None of it is a credential and none of it is a fact about the harness, which is the test that applies here. The as-sent figures in the table are unchanged, since scrubbing happens after the prompt is sent.

Only the scrubbed prompts under prompt/ are committed. A capture's own folder stays local: capture/.gitignore excludes every subdirectory and index.json, because the request bodies carry session and message ids and the raw runs carry account and device ids. Run the command and the folder appears; clone the repository and you get the tool and the prompts.

Every file is prefixed with its capture's name, so a file pulled out on its own still says which model and which mode it came from:

filewherescrubbedwhat
<model>-system-prompt.mdcapture/<name>/ and prompt/<HARNESS>/yesthe prompt on its own, nothing else
<name>-prompt-annotated.txtcapture/<name>/yessame text with block boundaries and char counts
<name>-request.jsoncapture/<name>/yesthe whole request body as sent
<name>-tools.jsoncapture/<name>/yestool definitions
<name>-meta.jsoncapture/<name>/n/arun id, sizes, token counts, tool names
trace/capture/<name>/nothe raw orca run. holds account and session ids

prompt/ groups by harness, one folder per agent, so the file inside is named for the model alone: prompt/CLAUDECODE/claude-fable-5-1-system-prompt.md, and the -p variant beside it as claude-fable-5-1-print-system-prompt.md. Two harnesses can serve the same model, which is what the harness folder disambiguates โ€” prompt/CODEX/gpt-5-6-sol-system-prompt.md and prompt/OPENCODE/gpt-5-6-sol-system-prompt.md are different prompts for one model. Under capture/ the same collision needs --dir, since there is no harness level there.

Only trace/ is unsafe to share, and capture/.gitignore keeps it out of commits. Everything else runs through the scrubber, and a capture aborts rather than writing a file if anything identifying survives the pass.

Each capture folder's own README.md is generated from its meta.json, so node capture/capture.mjs --index refreshes the file tables after a rename instead of letting them drift.

What -p means

-p / --print is Claude Code's non-interactive mode: the prompt goes in as an argument, one turn runs, the answer goes to stdout, and the process exits. No terminal UI. It is what a script or a CI job uses, and what the Agent SDK drives.

It is a different prompt, not the same prompt in a different shape. The billing header says cc_entrypoint=sdk-cli rather than cli, and the identity line changes from You are Claude Code, Anthropic's official CLI for Claude. to You are a Claude agent, built on Anthropic's Claude Agent SDK. Interactive mode then adds the ! <command> hint, the whole Scratchpad Directory section, a gitStatus block when the working directory is a repository, and four tools: Artifact, AskUserQuestion, EnterPlanMode, ExitPlanMode. Its injected role:"system" turn grows from 7,585 to 11,529 characters, carrying more agent types, the artifact and design skills, claude-in-chrome, and a paragraph about the active permission mode.

Both are worth keeping. Interactive is the prompt behind daily use; -p is the one you actually get when you call the harness from a script.

Scrubbing

Replacements are derived from the machine, not hard-coded, so the script is portable: home directory, username, git name and email, the gateway host from ~/.config/orca/config.json, the Claude Code project slug, the OS build, then generic rules for emails, uuids and hex runs of 32 or more. Order matters, since the memory directory sits inside the home directory.

Placeholders currently in use: {{CWD}}, {{HOME}}, {{TMP}}, {{CLAUDE_HOME}}, {{CLAUDE_PROJECTS}}, {{CODEX_HOME}}, {{PROJECT_SLUG}}, {{GIT_USER}}, {{OS_BUILD}}, {{RECENT_COMMITS}}, {{EMAIL}}, {{UUID}}, {{HEX}}, {{USER}}, {{GATEWAY}}.

Two things the script exists to get right

The interactive prompt is a different prompt. A harness only assembles it when stdin is a real console, and a pipe is not one, so a plain orca record claude always captures the -p variant. Interactive mode holds the proxy open with orca attach and has PowerShell launch the agent in a console of its own. For Claude Fable 5.1 that is the difference between 20,806 and 26,054 chars, and between 29 tools and 35.

The first request with a prompt in it is the wrong request. Claude Code opens with a title generator that ships four system blocks and a naming spec of its own. The agent turn is the one that carries tool definitions, so that is what identifies it.

The working directory is part of the prompt

Interactive capture refuses to start in a directory Claude Code has not been trusted in, and says so in a tenth of a second rather than waiting out the timeout:

capture failed: claude has not been trusted in C:\...\scratch, so it would stop on the trust dialog.
  start it there once by hand and accept, then run this again.
  capture in a directory you actually work in: the working directory is part of the
  prompt, and a non-repository loses the gitStatus block entirely.

Trust is one boolean in ~/.claude.json, so the script could set it. It deliberately does not. Writing it makes capturing in a throwaway directory the easy path, and that produces a worse capture: a temporary non-repository loses the whole gitStatus block, which cost 4,271 characters on a measured Opus 5 run. -p never asks, which is why non-interactive captures work anywhere.

--dangerously-skip-permissions is not a way around it either. It adds a paragraph about the active permission mode to the injected system turn, so it changes the prompt you are trying to read.

When the turn fails but the capture does not

A turn can come back overloaded_error inside a 200 stream. The prompt is still captured in full, because it travels in the request, but there is no token count for it. The script says so rather than just omitting the line:

  note: the agent's turn came back overloaded_error, so there is no token
        count for it. the prompt is unaffected - it travels in the request.

Documents

  • CURSOR-HTTP2.md - Cursor: HTTP/2 interception, a protobuf wire format, and a prompt that arrives in the response.
  • CAPTURE-RUNBOOK.md - the manual procedure in eight steps, with the pitfalls and how to prove a capture is genuine. Read it to port this to another harness or another platform.
  • capture-run.gif - one capture.mjs run, rendered by render-run.mjs.
  • fable-capture.gif, fable-capture.mp4 - the manual procedure as a screencast, rendered by render.mjs.

capturing the Fable 5.1 system prompt

Not covered

Interactive capture is Windows-only for now; it depends on Start-Process handing a console application a real console. On Linux and macOS a pipe sits behind a pty, so one orca record should reach the interactive prompt directly, and the profile in capture.mjs would need a branch for it.

Codex TUI capture works through the same path but is not run here by default: this installation sets approval_policy = "never" with sandbox_mode = "danger-full-access", and an interactive session under those settings is a different risk from a codex exec that answers one word. Pass --interactive to do it anyway.

A prompt captured this way is the one the local harness sent. A service that injects its own prompt server-side, claude.ai for instance, is invisible to this method by construction.