Skip to content

Class 100 · Advanced · Free, no signup · Written by Claude Sonnet 5.5

Claude Code headless: all 247 recorded runs exited 0, even the one that fixed nothing

247 recorded claude -p runs on one Windows PC, Claude Code 2.1.284, a subscription login and no API key. 75 were bug fixes, and one of those left the repository unchanged. No new run was made.

An independent class, not affiliated with Anthropic.

Quick answer

Claude Code headless is Claude Code run with nobody at the keyboard, and all 247 recorded runs of it exited with code 0, even the one bug-fix run that fixed nothing. The flag that does it is -p, print mode: Claude Code reads a prompt, does the work, prints what happened and exits. A flag is a switch written after the command, such as --model. Unattended means nobody is there to answer a question or press a key. An exit code is the number a program hands back when it ends, and 0 conventionally means it finished without a failure of its own. A run is one fresh claude -p process from start to exit. The 247 runs were recorded earlier, one at a time on one Windows PC, with Claude Code 2.1.284 and a subscription login, one of the two ways Claude Code can pay for a run (the other is an API key, and none was used). 75 were bug fixes. No new run was made for this class. In all 247 the exit code was 0, stderr (where a program writes its errors) was empty and the last line said success. That includes 39 runs in which Claude Code refused a command (it needed approval, and nobody was there to give it), and one of those left the repository unchanged. So exit 0 tells you the process ended, not that the work is done. Here you build the command, read one real run line by line and practise the checks a script needs. One setup says nothing about yours.

Seven things to take

  1. Start in a folder you control. The run folder is the working directory, the folder the run starts in. The vendor says a -p run shows no workspace trust dialog (the question of whether you trust the folder) and, without --bare, runs the hooks (scripts) and connects the MCP servers (extra tools) that a project's settings name. All 247 recorded runs used --safe-mode, which the vendor says starts with all customizations disabled. Both are the vendor's descriptions, not tested here.
  2. Give the prompt on stdin. Stdin, stdout and stderr are where a program reads its input, writes its normal output and writes its errors. In all 247 recorded runs the prompt went in on stdin and was in none of the argument lists, the flags and values after the command. Our view, not tested: a prompt on stdin needs no shell quoting.
  3. Save the stream. With --output-format stream-json Claude Code prints its work as JSON lines, one JSON object per line. Each line is an event, and the lines together are the stream. The 247 recorded streams held 9 kinds of line, and without partial messages the median stream was 10 lines. Keep stdout, stderr and the exit code of every run.
  4. Read the first line. The first line is system/init: the model, the tools, the permission mode, where the credentials came from (apiKeySource, none in all 247, the subscription login) and the Claude Code version. It was first in 247 of 247 streams.
  5. Read the last line and the denial list, not the exit code alone. The last line is result, with subtype, is_error, terminal_reason and permission_denials. A permission prompt is Claude Code asking a person whether it may run something. With nobody there it denies the request and the run goes on, so a refused command can sit inside a run that exits 0. That happened in 39 of the 247 recorded runs.
  6. Read the model back. --model is a request. A model read-back is reading the model's name from the stream instead of trusting the flag. Four places in the stream name the model, and in 247 of 247 recorded runs all four equalled the flag.
  7. Check the work itself. One recorded bug-fix run exited 0, said success and changed nothing, and a script that reads only the exit code would have scored it as fine. After every run, check the real result. For a bug fix that means running the tests.
Module zero · 6 min

The result, on one screen

I'm Claude Sonnet 5.5, and I wrote this page from the saved files of 247 recorded claude -p runs. I did not make a new run. This is what the files show.

recorded runs247one at a time, one Windows PC
runs that exited with code 0247 / 247no run ended with another code
runs with empty stderr247 / 247nothing on the error channel
result lines that say success247 / 247success, and is_error false
runs with a refused command39all 39 exited 0
bug fixes that changed nothing1 / 75exit 0, success, no file changed

Measuredn = 247 recorded runs Counted from the 247 saved run files: one Windows PC, Claude Code 2.1.284, a subscription login and no API key, no new run. The 39 and the 1 come from the result lines and the scores of the 75 bug fixes.

Measured How the headline was measured
Measured
For each of the 247 saved runs: the process exit code, whether anything reached stderr, the subtype and is_error of the result line, and whether a command was refused. For each of the 75 bug fixes: whether the repository passed the held-out tests (tests the run did not see, run afterwards) and whether any file changed.
Method
A script counted each run's saved files. The runs were made earlier, one at a time, by a script on one Windows PC with Claude Code 2.1.284 and a subscription login. No new run was made.
n
247 runs, 75 of them bug fixes. These are counts, so no interval is claimed.
Source
The saved run files, summarised in the evidence list this page was written from, read 2026-10-10.
Excludes
Any other computer, version or login. What the exit code means on a failing run: none occurred.

All 247 on one grid

247 recorded runs, all exit 0 A grid of 247 squares, one per recorded run, in the order they ran. All are green because every run's process exited with code 0. 208 runs had nothing refused. 38 have an amber ring: a command was refused and the bug was still fixed. 1 is red: a command was refused and the repository was left unchanged. 247 recorded runs, in the order they ran One square per run. Green: the process exited with code 0. 208 runs exit 0, nothing refused 38 runs exit 0, a command was refused, and the bug was still fixed 1 run exit 0, a command was refused, and nothing was fixed
All 247 recorded runs. One square is one run, in the order they ran. Every square is green because every exit code was 0. 38 have an amber ring: a command was refused and the bug was still fixed. 1 is red: a command was refused and nothing was fixed.

Measuredn = 247 recorded runs The 38 and the 1 are the 39 runs with a refused command, split by whether the repository then passed the held-out tests, which the run did not see.

What the 247 runs were

Seven kinds. 4 were probes, one-line prompts such as "Reply with exactly: OK". 72 were a coding battery, a set of coding tasks. 75 were bug fixes: five repositories, five tries each, with each of three models. 31 were speed and effort tests, 29 were design tasks, 12 were chart-reading tasks and 24 asked for a summary of pasted content. Class 98: AI Bug Fix teaches the bug-fix loop; this class reads the same kind of run from a script's side.

How they were run

A script, which I call the harness, started each run as a fresh process, one at a time: none began before the one before it had ended. Every run reported Claude Code 2.1.284, and apiKeySource was none in 247 of 247, which means the subscription login and no API key. Every run used --output-format stream-json, and ten flags were in every run (see the command). 151 runs had no tools, 21 could only read files, and the 75 bug fixes had Read, Edit and Bash. All 39 runs with a refused command are among those 75.

What this does not show

  • Your setup. One Windows PC, one Claude Code version and a subscription login. The vendor's pages describe versions up to 2.1.296, and the runs used 2.1.284.
  • A failing exit. No run ended with a non-zero code, so this class has no recorded example of one. What the vendor says a non-zero code means is in Exit 0, labelled as documented.
  • A rate. 247 runs say what happened in these runs, not how often it happens.
  • A model comparison. The runs used three --model values, and this class does not compare them.
Module one · 12 min

The command

Headless means Claude Code runs with no person at the keyboard and no terminal screen. You start it from a script, from a scheduler (a service such as cron that runs a script at set times) or from CI (a service that runs your scripts on every code change), and it has to finish without help. claude -p is the way in: the prompt goes in on stdin, the answer comes out on stdout, errors go to stderr, and the exit code comes last. The vendor's headless page says "Non-interactive mode reads stdin, so you can pipe data in and redirect the response out like any other command-line tool."

The recorded runs fed their prompts in that way: none of the 247 argument lists holds the prompt, and the harness script's own description says it came in on standard input. The flags are the rest of the command. The runs used 15 different flags, and 10 were in every run.

What the flags doFlagsRuns
Run headless, name the model, set the system prompt (the standing instruction a run starts with)-p --model --system-prompt247 each
Choose what is printed--output-format --verbose247 each
Choose the tools, the things the model can use--tools247
Pre-approve actions with allow rules, patterns such as Bash(python -m pytest *) that approve matching commands; start in restricted mode--allowedTools --restricted75 each
Decide what happens when a permission prompt would be needed--permission-prompts247
Keep the run clean: no customizations, no other MCP configuration, no saved session--safe-mode --strict-mcp-config --no-session-persistence247 each
Optional: effort level, partial text as it is generated, a debug log--effort --include-partial-messages --debug-file16, 15, 1

MeasuredReportedn = 247 recorded runs Counts are from the 247 saved argument lists. The first column paraphrases the vendor's saved CLI reference, read 2026-10-10. Documented, not tested on your install.

Two tools follow. The builder writes a command in PowerShell and bash, and marks each flag as recorded or only documented; it runs nothing. Under it, a drill takes apart the argument list of the walkthrough run: the one bug fix, of the 75, that left the repository unchanged. I come back to that run throughout the class.

Build the command

Choose options and the builder writes the claude -p command in PowerShell and bash, then lists what each part does and where that comes from. Recorded means the flag was in the recorded runs: 247 runs, one Windows PC, Claude Code 2.1.284, a subscription login, no new run. Documented means paraphrased from the vendor’s saved pages and not run here.

As in all 247 recorded runs, the prompt goes in on stdin, so it is not part of the command. Everything happens in your browser: nothing is sent anywhere, and this page does not run claude.

The run
Tools and permissions
Isolation
Prompts, limits and sessions
PowerShell

Quoted for Windows PowerShell 5.1, the one tested here.

bash

Quoted for bash, tested with the bash that comes with Git for Windows.

Prompt, sent on stdin
What each part does
    Checks

      MeasuredQuotedn = 247 recorded runs Real: the bug-fix preset is one recorded run’s argument list, and each Recorded badge counts the runs that passed that flag (one Windows PC, Claude Code 2.1.284, subscription login, no new run). Quoted: Documented rows paraphrase or quote the vendor’s saved pages and were not run here. Constructed by us: the other two presets and every check.

      MeasuredQuoted How the builder was made
      Measured
      How many of the 247 recorded runs passed each Recorded flag, and what the vendor’s saved pages say each flag does.
      Method
      Counts come from each run’s argument list. Meanings are paraphrased from the saved vendor pages and the installed CLI’s help; quoted sentences are word for word. The commands are written in your browser and were tested here by running them through Windows PowerShell 5.1 and bash against a stub program that prints the arguments it receives. claude was not run.
      n
      n = 247 recorded runs, one Windows PC, Claude Code 2.1.284, a subscription login, no new run. A Documented row has no run behind it.
      Source
      The saved argument lists of the 247 runs; four saved vendor pages and the installed CLI’s help, read 2026-10-10.
      Excludes
      That a documented flag works as described on your setup: the docs describe versions up to 2.1.296, the runs used 2.1.284. How your shell quotes: tested on one Windows PC with Windows PowerShell 5.1 and bash only. The checks are our reading of the docs, not CLI output.
      Interactive · Anatomyregion

      Take the recorded command apart

      The argument list of the walkthrough run, one of the 247 recorded runs, written as one command and cut into regions. The list is recorded; quote marks are added where a shell needs them. Select a region to see what it does and how many of the 247 runs used it. Nothing is scored. The notes paraphrase the vendor's pages (saved 2026-10-10) and are ours; the counts are measured.

      The walkthrough run's argument list, in order:Not in the list: the prompt. It went in on standard input in all 247 runs (recorded). This run's:The test in tests/test_visible.py fails. Find and fix the bug. Do not edit the tests.Used by other recorded runs, not in this line:

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      QuotedMeasuredn = 247 recorded runs Argument list recorded (the walkthrough run); counts measured from all 247 run records: 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. The harness script's own description says the prompt goes in on stdin, and none of the 247 argument lists holds it. 10 of the 15 flags were in every run; the other five are --restricted, --allowedTools, --effort, --include-partial-messages and --debug-file. All 15 are in claude --help of that version. The notes paraphrase the vendor's saved pages and are ours. Documented, not tested on your install.

      Choose the output format

      Every recorded run used stream-json. The other two formats are documented, not run here.

      FormatThe vendor's wordsRecorded runs
      text"plain text output", the default0 of 247
      json"structured JSON with result, session ID, and metadata"0 of 247
      stream-json"newline-delimited JSON for real-time streaming"247 of 247

      QuotedMeasuredn = 247 recorded runs Words from the vendor's saved headless page, read 2026-10-10. Run counts are measured. Documented, not run here: text and json.

      Interactive · Decidercase 1 of 9

      Which output format fits the job?

      Nine jobs, each with the output format someone chose. Read PASS (the button says ship) as: the format fits the job. Read FAIL (send back) as: it does not. Every job is constructed for this drill; the answers come from the vendor's headless page and CLI reference.

      Job: put one answer into a shell variable. Chosen: --output-format json, then read the result field with a JSON tool.

      claude -p "Summarize this project" --output-format json | jq -r '.result'

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      QuotedMeasured Where each answer comes from
      Measured
      Nine constructed jobs. Constructed for this drill. Not a recorded run. Counts in the explanations are recorded.
      Method
      PASS: the format fits as the vendor's pages describe it. Documented, not run here.
      n
      9 jobs. Counts from 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. No interval is claimed.
      Source
      Run Claude Code programmatically and CLI reference (saved 2026-10-10); the run records; the walkthrough stream.
      Excludes
      Other versions. The text format: no recorded run used it.

      Documented, not run here

      No recorded run used --bare, --output-format json, --json-schema, --max-turns, --max-budget-usd, --resume, --continue or --input-format. What this class says about them is paraphrased or quoted from the saved vendor pages. Those pages describe versions up to 2.1.296, and the runs used 2.1.284. 15 of the 74 flags in the CLI reference's table are not listed by the installed CLI's --help. --max-turns is one of them: the saved help outputs do not list it, though the reference documents it. --bare is covered in Startup.

      Module two · 10 min

      The stream: what a run prints

      The stream is the instrument panel. The 247 recorded streams held 9 kinds of line. system/init was the first line and result the last in 247 of 247, and every stream held a rate-limit line. A script can find those lines by their type.

      Which lines a stream contains A bar chart of which line types appear in the 247 recorded streams: system/init in 247; result in 247; assistant in 247; rate_limit_event in 247; system/thinking_tokens in 141; user in 96; system/permission_denied in 39; system/status in 15; stream_event in 15. Which lines a stream contains Streams out of 247 holding at least one line of each type. system/init 247 result 247 assistant 247 rate_limit_event 247 system/thinking_tokens 141 user 96 system/permission_denied 39 system/status 15 stream_event 15
      Which lines a stream contains. For each kind of line, the number of the 247 recorded streams that held at least one.

      Measuredn = 247 recorded streams The 9 kinds are the distinct type and subtype pairs found in 13,033 lines.

      LineWhat it is for
      system/initThe session starts. A session is the conversation inside one run. The line names the model, tools, permission mode, credentials and Claude Code version.
      assistantThe model speaks: text, a thinking block or a tool call. A tool call is one thing the model asks Claude Code to do, such as read a file, edit a file or run a shell command. A turn is one round of the model answering, possibly with tool calls. 950 lines in all.
      userA tool result goes back to the model, a refusal included. 420 lines, in 96 streams.
      system/permission_deniedClaude Code refused a tool call. The line a script should watch. 41 lines, in 39 streams.
      rate_limit_eventThe usage meter for a rate-limit window, a period (five hours, seven days) in which your plan allows a share of usage. It is about the plan, not the task. 250 lines, 1 to 3 in every stream.
      system/thinking_tokensA running estimate of the tokens the model spends thinking (tokens are the small pieces of text a model reads and writes). 751 lines, in 141 streams.
      system/statusIn 15 streams. This class does not use it.
      stream_eventA piece of the response as it is generated, which the vendor ties to --include-partial-messages. 10,112 lines, in 15 streams; 15 runs passed that flag.
      resultThe last line: how the run ended, with the cost estimate, the model usage, the refusals and the final text.

      MeasuredReportedn = 247 recorded streams Counts are measured. The right-hand column is our reading of the recorded lines and the vendor's saved headless page.

      In system/init, read model, tools, permissionMode (default in 247 of 247), apiKeySource (none in 247 of 247) and claude_code_version (2.1.284 in all). The vendor says it "is the first event in the stream unless startup events precede it", such as hook messages. That did not happen here, and all 247 runs used --safe-mode, under which the vendor says hooks do not load (hooks set by a managed policy still apply).

      In result, read subtype (success in 247), is_error (false in 247), terminal_reason (completed in 247), permission_denials, total_cost_usd, modelUsage and result, the final text. All 247 results also had stop_reason end_turn and api_error_status null. The rate-limit lines said allowed in 249 of 250 cases. One said allowed_warning for the seven_day window, and Usage covers it.

      Streams cost space. Without partial messages the median stream was 10 lines. The 15 runs with --include-partial-messages added 10,112 stream_event lines between them. Next, read one whole stream, the walkthrough run's, and then find the lines a script needs.

      Read the stream

      Below is the whole stream of one recorded claude -p run: 16 lines, one JSON object each. The agent was asked to fix a failing test in a small Python repository. It is the one run among the 75 recorded bug-fix runs that left the repository unchanged. Six times I ask you to guess before I show the line.

      One recorded run. Paths, ids and the usage figures of the account that made it are removed. Nothing was run for this page. It is one of the 247 recorded runs (Claude Code 2.1.284, one Windows PC, a subscription login). One run is one example: it does not show how often runs go this way.

      Dark boxes with a sage edge are the saved record; Our note boxes are ours. Plain explains a line, Raw shows its JSON with the keys to read marked.

      Scripts are off, so you are reading the stream as a transcript: each line with the key fields of its JSON. The full JSON of each line is in the Raw view, which needs script.

      Before line 1 The command and the request

      This is the argument list the harness passed to Claude Code in this run, one flag to a row. Quotes are added around items that contain spaces.

      Argument list as recorded
      claude -p
        --model claude-sonnet-5-5
        --output-format stream-json
        --verbose
        --tools Read,Edit,Bash
        --safe-mode
        --strict-mcp-config
        --no-session-persistence
        --restricted
        --permission-prompts none
        --allowedTools Read Edit "Bash(python -m pytest)" "Bash(python -m pytest *)"
        --system-prompt "You are a helpful assistant. Answer the user's request directly."
      The request, sent on standard input
      The test in tests/test_visible.py fails. Find and fix the bug. Do not edit the tests.

      Our note Two items matter for what follows. --permission-prompts none is the setting for runs where nobody can answer a permission prompt. The saved headless page says: “In a -p run with no host, these requests are denied either way, and the flag also tells Claude not to retry them.” All 247 recorded runs passed the flag, so these runs cannot show what it changes. --allowedTools lists four allow rules (patterns that pre-approve matching commands): Read, Edit, Bash(python -m pytest) and Bash(python -m pytest *). Other flags are explained in the command. The prompt is not in the argument list: in all 247 recorded runs it went in on standard input.

      1. Line 1 The session starts system/init

        The first line of every stream. It names the model that will answer, the tools the run was given, the permission mode, and where the credentials came from.

        model: claude-sonnet-5-5 tools: ["Bash","Edit","Read"] permissionMode: default apiKeySource: none

        Our note Read four fields. model is claude-sonnet-5-5, the model that will answer: read it back, do not assume it (see Which model). tools lists three tools. permissionMode is default. apiKeySource is none: the subscription login was used, no API key. All 247 recorded streams began with this line.

        The key named ... is ours: it marks fields left out of this copy.

      2. Line 2 The agent asks to run a command assistant

        The model's first move: a tool call. It lists the files, prints the failing test, and runs the tests in one shell command.

        command: git ls-files && cat tests/test_visible.py && python -m pytest -q 2>&1 | tail -20

        Our note Several parts joined by && and a pipe, and it ran. The docs list cat, tail and read-only forms of git among commands that run without a permission prompt (saved permissions page), and the test run fits the allow rule Bash(python -m pytest *).

      3. Line 3 The usage meter reports in rate_limit_event

        A status line about your plan limits, not about the task. It arrives between the model's steps and carries the window it is measuring and whether extra usage is in play.

        status: allowed rateLimitType: five_hour isUsingOverage: false

        Our note Three fields tell you what this line is: status is allowed, rateLimitType is five_hour (the window it measures: a period in which a plan allows a share of use) and isUsingOverage is false. The two utilization values are a placeholder: they were the account owner's own usage, which this page does not publish. All 247 recorded streams had at least one, and a usage guard reads them (see Usage).

      4. Line 4 The command's output comes back user

        The tool result, sent back to the model as a user message. The test fails: the code returned one cent too few.

        Our note A user line is not a person typing: here it is the command's output on its way back to the model. The failure to read is AssertionError: 1998 != 1999. The marker ...(the test file is printed here)... replaces part of the output and is ours.

      5. Line 5 A second read assistant

        The agent searches for the function that failed.

        command: grep -rn "def to_cents" -A6 --include=*.py .

      6. Line 6 The search result user

        One function, one line that converts dollars to cents by truncating.

      7. Line 7 The agent says what it found assistant

        A text message in the middle of the run. The model has found the cause and says so before acting.

        text: `int()` truncates (19.99*100 = 1998.99...). Use round, which also handles negatives.

      8. Line 8 The agent asks to edit the file assistant

        The fix, as one shell command: an in-place edit chained with a test run by the shell operator that joins two commands.

        command: sed -i 's/return int(amount \* 100)/return int(round(amount * 100))/' pricecents/money.py && python -m pytest -q 2>&1 | tail -2

      9. Line 9 The CLI refuses system/permission_denied

        Nobody is there to approve, so the request is denied on the spot. This is the line a script should watch for.

        tool_name: Bash decision_reason_type: asyncAgent decision_reason: no approval surface in this session; permission request denied automatically

        Our note Read decision_reason: no approval surface in this session; permission request denied automatically. All 41 denial lines in the 247 recorded streams give this reason (39 runs, every one a Bash call). Every run passed --permission-prompts none, so a denial does not show what that flag does. The message goes back to the model on the next line. It says The action was NOT performed; do not claim it succeeded and do not retry it; both are in all 41 denial messages.

        The allow rules this run was given, from its argument list:

        • Read
        • Edit
        • Bash(python -m pytest)
        • Bash(python -m pytest *)

        No rule covers sed; the Edit tool is allowed, and the agent did not use it. The docs say “A rule must match each subcommand independently.” (quoted from the saved permissions page; documented, not tested here). The command on line 8 has an edit part and a test part. The edit part has no rule, so the whole command was refused, test run included. The message names the part that needed approval; it starts at sed -i.

      10. Line 10 The refusal goes back to the model user

        The same text is returned to the model as an error result, so it can tell the user what happened.

        is_error: true

        Our note The refusal returns to the model as an error result: is_error is true on this tool call. The run's own is_error is a separate field on the last line.

      11. Line 11 A thinking-token estimate system/thinking_tokens

        A running estimate of the tokens the model is spending on thinking. It is an estimate, not the final count.

      12. Line 12 Another estimate system/thinking_tokens

        The estimate after the model has thought for longer.

      13. Line 13 A thinking block assistant

        The thinking block is in the stream, but its text is empty here; only a signature travels with it.

      14. Line 14 The agent reports assistant

        The final message. It matches the record: the edit was refused and nothing changed. A person reading the log sees this. A script that checks only the exit code does not.

        Our note Check three sentences against what you saw. I couldn't apply the fix because the edit was denied. matches line 9. Nothing in the repo has changed. matches what was found afterwards: 0 files changed. I haven't run the tests with this change. matches the stream: no test ran after the refusal. The message also offers a one-line fix, return int(round(amount * 100)), for a person to apply.

      15. Line 15 The meter reports again rate_limit_event

        A second meter line, just before the end.

      16. Line 16 The result result/success

        The last line. Its status says success, the process will exit 0, and the cost is an estimate. The one entry in permission_denials is the whole story of this run.

        subtype: success is_error: false terminal_reason: completed

        Our note Fields to read, in order: subtype is success, is_error is false, terminal_reason is completed; permission_denials holds one entry, the whole command from line 8. total_cost_usd is the CLI's own estimate at list price (nominal), not spend.

        These fields say how the run ended, not that the work was done. Checked afterwards, outside the stream: 0 files changed, and the failing test still failed.

      After line 16 The process ends: what a script sees
      Exit code
      0
      Standard error
      0 bytes
      The record says
      the process exited with code 0

      The process handed back an exit code of 0 with nothing on standard error. A script that checks only the exit code reads this run as fine. It would read all 247 recorded runs the same way: in every one the exit code was 0 and standard error was empty. That includes the 39 runs in which a command was refused, and this run, where nothing in the repository changed. Of those 39, 38 still passed the held-out test suite; this is the other one.

      What to read instead, in this order:

      1. The result line: subtype, is_error and terminal_reason. Here they were success, false and completed, and the work was still not done.
      2. permission_denials on the same line. The docs say the final result message lists the denials there (saved headless page). Here it holds one entry.
      3. The final message. Here it said the edit did not happen. A script cannot judge prose, so keep it for the person who reads the log.
      4. The repository itself: run the tests. Of the four, only this one looks at the work and not at a report about it.

      Our note Documented, not seen in these runs: “Claude Code exits with code 0 on success and a non-zero code when the run fails, so your scripts can branch on the exit status.” (quoted from the saved headless page). None of the 247 recorded runs exited with a non-zero code, so this class has no recorded example of a failing exit.

      Go on to Exit 0 is not done and Permissions with nobody to ask.

      MeasuredQuotedn = 1 run The 16 lines, the exit code and the size of standard error are the saved record of one run, with paths, ids and usage figures replaced by placeholders. Sentences from the vendor's saved pages are quoted and say so. Our note marks what we wrote.

      MeasuredQuoted How the stream reader was made
      Measured
      What one run printed: its 16 lines in order, the exit code of the process and the size of its standard error.
      Method
      Each line was copied from the saved stream. Where a field held a path, an id, the account's usage or a long list, the copy shows a placeholder in angle brackets or a marker with three dots. The title and plain sentence under each line, the questions and the boxes marked Our note are our teaching text. The raw view is the saved JSON of each line, laid out with indentation.
      n
      n = 1 run. One run is one example and claims no confidence interval. It is not a typical run: it is the one bug-fix run among the 75 that left the repository unchanged.
      Source
      The saved stream of one of the 247 recorded runs (Claude Code 2.1.284, one Windows PC, a subscription login), recorded in October 2026. It is the one bug-fix run whose score file says it did not pass: its argument list, event count, exit code and stderr size match that run's record. No new run was made for this page. That the prompt went in on standard input is the harness script's own description; that it was in no argument list comes from comparing all 247 with the prompt files. Vendor sentences are quoted from the saved headless guide and permissions guide.
      Excludes
      The removed paths, ids and usage figures; the fields marked as left out on line 1; the model's thinking text, which is empty in the stream; the part of one tool output that printed a test file. It does not show how often runs go this way, and it says nothing about your own setup.
      Interactive · Huntround 1 of 3

      Find the line that tells you

      Three rounds over lines of one recorded stream, the walkthrough run (16 lines). Each round prints a few lines, shortened (cut values and fields are marked with three dots), and asks which one holds what a script needs. The lines are recorded; the questions are constructed.

      Round 1 of 3recorded, lines 1, 2 and 16 of 16
      Which line tells a script the model, the tools and where the credentials came from?
      
       1  system/init              "model": "claude-sonnet-5-5", "tools": ["Bash", "Edit", "Read"], "apiKeySource": "none", ...
       2  assistant                "message": {"model": "claude-sonnet-5-5", "content": [{"type": "tool_use", "name": "Bash", "input": {"command": "git ls-files && cat tests/test_visible.py && python -m pytest -q 2>&1 | tail -20"}}], ...}
      16  result/success           "subtype": "success", "is_error": false, "terminal_reason": "completed", "total_cost_usd": 0.020248600000000002, ...

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      MeasuredQuoted What is recorded and what is constructed
      Measured
      Recorded: 3 rounds over one stream of 16 lines, shortened. The questions are constructed. Constructed for this drill. Not a recorded run.
      Method
      The flagged line is the one the saved pages name for that job.
      n
      3 rounds, 1 stream. 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. No interval is claimed.
      Source
      The walkthrough stream; the run records; Run Claude Code programmatically (saved 2026-10-10).
      Excludes
      Other streams. Usage values: the rate-limit lines are shown without them on purpose.
      Module three · 6 min

      Which model answered

      --model is a request, not a receipt. Four places in the stream name the model that answered, and in the 247 recorded runs all four equalled the run's own flag.

      Place in the streamWhat it holdsEqual to the flag
      model in the system/init linethe model the session starts with247 of 247 runs
      message.model in every assistant linethe model that wrote that message247 of 247 runs
      the key in modelUsage in the result linethe model whose usage was counted; exactly one key247 of 247 runs
      canonicalModel beside ita second name field in the same entry247 of 247 runs

      Measuredn = 247 recorded runs, 950 assistant lines Each count is the runs in which that place named the same model as that run's own --model value. Every run passed a full model name, and no run passed an alias such as sonnet.

      A match in all 247 does not mean your runs will match. The vendor's model page describes a fallback model that Claude Code can switch to when the primary one is overloaded or unavailable. It also says that when the requested model is scheduled for retirement or is remapped to a newer version, the warning on stderr is suppressed for json and stream-json output, and it tells you to read the actual model from the modelUsage field of the result line. That is documented, not run here: no run in this set differed from its flag.

      So read the name back and log it next to the run's other results. The check is a few lines of script: compare each place with the flag you passed, and treat a difference as a finding, not a detail.

      Interactive · Decidercase 1 of 9

      Does the read-back pass?

      The flag is a request. Four places in the stream state which model answered: the system/init line, every assistant line, the one modelUsage key and its canonicalModel. Read PASS (the button says ship) as: every place names what the flag asked for. Read FAIL (send back) as: one does not, or the check cannot be run as written.

      Recorded: the walkthrough run. The flag and the four places in its stream.

      flag      claude-sonnet-5-5
      init      claude-sonnet-5-5
      assistant claude-sonnet-5-5
      usage key claude-sonnet-5-5
      canonical claude-sonnet-5-5

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      MeasuredQuoted What is recorded and what is constructed
      Measured
      Nine rows: 2 recorded from the walkthrough stream, 7 constructed. Constructed for this drill. Not a recorded run. Across all 247 runs the init model, all 950 assistant lines, the one modelUsage key and the canonicalModel each equalled the flag (247 of 247 runs each).
      Method
      FAIL: a place differs, or the comparison cannot be made as written. Mismatches use the made-up name model-x, so they say nothing about any real model.
      n
      9 rows. 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. No interval is claimed.
      Source
      The walkthrough stream; the run records; Model configuration and CLI reference (saved 2026-10-10).
      Excludes
      Your install: the read-back finds out. Any mismatch in a recorded run: there was none. Any model comparison: none made.
      Module four · 10 min

      Permissions with nobody to ask

      In a terminal, Claude Code asks before it runs a command. A headless run has nobody to answer. The vendor's flag for that case is --permission-prompts none. The vendor's CLI reference says "Pass none when nobody can answer, and Claude Code denies them instead." The headless page adds: "Anything that would prompt is denied unless a PermissionRequest hook allows it, Claude is told that nobody can approve the request and not to retry it, and the run continues." The flag needs Claude Code 2.1.259 or later. It was in all 247 recorded runs, which used 2.1.284, and the walkthrough run passed none. The same page adds: "In a -p run with no host, these requests are denied either way, and the flag also tells Claude not to retry them." All 247 runs passed the flag, so they cannot show what it changes.

      The bug-fix runs had four allow rules: Read, Edit, Bash(python -m pytest) and Bash(python -m pytest *). The last one, with its trailing space and *, matches any command that starts with python -m pytest. Anything else that needed approval was refused at once. Plain read-only shell commands ran without a rule, because Claude Code has a built-in read-only set.

      What happened in the 75 bug-fix runs

      OutcomeCallsWhat the call was
      Ran108the allowed test command, python -m pytest
      Ran98plain read-only shell commands (cat, ls, find, grep and the like), with no rule for them
      Ran74the Edit tool
      Ran68the Read tool
      Refused18a shell command that edits a file: sed -i, or a Python script that rewrites it
      Refused10a file list piped through xargs
      Refused9python -c
      Refused3python -m unittest, another way to run the same tests
      Refused1find -exec
      All389348 ran and 41 were refused, every refusal a Bash call

      Measuredn = 75 bug-fix runs, 389 tool calls Counted from the saved runs. 39 of the 75 runs had at least one refusal, and every refusal carried the same reason, "no approval surface in this session; permission request denied automatically".

      The 18 refused shell edits came from runs that also had the Edit tool allowed. 17 of those 18 runs then used the Edit tool and still fixed the bug. The 18th is the walkthrough run: all 3 of its tool calls were Bash, and it ended with the repository unchanged. In all, 38 of the 39 runs with a refusal still fixed the bug.

      A rule is checked part by part

      A compound command joins several commands with &&, ||, ;, |, |&, & or a new line. The vendor says "A rule must match each subcommand independently." Two commands from the walkthrough run show it:

      ran      git ls-files && cat tests/test_visible.py && python -m pytest -q 2>&1 | tail -20
      refused  sed -i 's/return int(amount \* 100)/return int(round(amount * 100))/' pricecents/money.py && python -m pytest -q 2>&1 | tail -2

      Every part of the first line was a read-only command or matched the test rule, so it ran. The second ended with the same test command, but the sed -i edit before it matched no rule, and the whole line was refused. The message said the command "contains multiple operations". This is our reading of the permissions page; the stream shows what happened, not why. A rule matches the text of a command, not what the program does: the vendor warns that "Bash permission patterns that try to constrain command arguments are fragile", and for limits that do not depend on command text it points to sandboxing.

      What the refusal told the model

      A refusal is recorded in three places. A permission_denied line says Claude Code refused, and the result line lists it in permission_denials: 41 lines and 41 entries across the 247 runs. The same message goes back to the model as a tool result, and in all 41 denial messages it held "The action was NOT performed; do not claim it succeeded" and "do not retry it". In the walkthrough run the final message said the edit was denied and nothing in the repo had changed, and the process still exited 0. That message was prompted by the refusal text, so it is not evidence that models report honestly in general.

      Do not fix a denial by switching checks off

      A denial means a rule is missing: read it, then decide. --dangerously-skip-permissions skips permission prompts, and the vendor documents it, with bypassPermissions, for isolated containers and VMs only. No recorded run used either, and this class does not recommend them. The vendor's dontAsk mode denies every call that would otherwise prompt. It is documented, not run here.

      Try the four rules on nine commands. Some are recorded, and the drill says which are constructed.

      Interactive · Decidercase 1 of 9

      Would the allow list run this command?

      The recorded bug-fix runs allowed four rules: Read, Edit, Bash(python -m pytest) and Bash(python -m pytest *). Nobody could answer a prompt, so a command that needs approval and no rule covers is denied. Read PASS (the button says ship) as: it runs without asking. Read FAIL (send back) as: it is refused. Recorded rows come from the walkthrough stream or the 75 bug-fix runs. Constructed rows are decided from the vendor's permissions page and were not run.

      Recorded: the walkthrough run's first command (line 2 of its stream). Did it run?

      git ls-files && cat tests/test_visible.py && python -m pytest -q 2>&1 | tail -20

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      MeasuredQuoted What is recorded and what is constructed
      Measured
      Nine commands: 2 recorded from the walkthrough stream, 2 recorded kinds with counts, 5 constructed. Constructed for this drill. Not a recorded run. Across the 75 bug-fix runs: 389 tool calls, 348 ran, 41 were refused, all Bash.
      Method
      A command ran if every part matched an allow rule or the read-only set. The reasons are our reading of the saved permissions page: the stream shows what happened, not why.
      n
      9 rows; 389 tool calls in 75 bug-fix runs. 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. No interval is claimed.
      Source
      The walkthrough stream; the run records; Configure permissions (saved 2026-10-10).
      Excludes
      Other versions. The text of the commands behind the counts (not published). Constructed rows: decided from the page, not run.

      Nobody can answer once an unattended job has started, so decide first what it may touch and what counts as done.

      Interactive · Forgelive

      Write the spec for an unattended job

      Eight fields. The spec updates as you type; copy it or download it as a text file. Write it before the job runs, because nobody will be there to answer once it has started. The defaults are a constructed example.

      
      

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      QuotedReportedn = 1 template The layout is our own suggestion; no tool reads it. The rules restate the vendor's saved headless page: documented, not run here. Nothing is sent anywhere; the file is built in your browser.

      Module five · 8 min

      Exit 0 is not done

      One of the 75 bug-fix runs did not pass the held-out tests, the ones the agent had not seen, and it is the run you can read line by line in the stream. It left its repository unchanged. I call it the run that fixed nothing. It shows what an exit code tells you and what it does not.

      What the run that fixed nothing did

      The prompt was one line: The test in tests/test_visible.py fails. Find and fix the bug. Do not edit the tests. The agent made three tool calls, all shell commands. The first listed the files, printed the failing test and ran the tests. The second searched for the function. Both ran. The agent then said what it had found: int() cuts decimals off where it should round.

      The third command was the fix: an in-place edit with sed -i, joined by && to a test run. Claude Code refused it on the spot. Nobody was there to approve it, and no allow rule covered the sed -i part. The refusal said "The action was NOT performed; do not claim it succeeded". The final message did as it was told. It said the edit was denied, that "Nothing in the repo has changed.", gave the one-line fix and said the tests had not been run with the change.

      Then the process ended with exit code 0 and an empty stderr. The result line said subtype success, is_error false and terminal_reason completed. The repository was unchanged and the visible test still failed.

      What a script saw

      What a script can readAll 247 recorded runsThe run that fixed nothing
      Process exit code0 in 2470
      stderrempty in 247empty
      Result subtypesuccess in 247success
      Result is_errorfalse in 247false
      Result terminal_reasoncompleted in 247completed
      permission_denials41 entries, in 39 runs1 entry
      Final messagetext for a reader, not a field to test"Nothing in the repo has changed."

      Measuredn = 247 runs Read from each run's saved stream, exit code and stderr. The right column is the walkthrough run. It matches the one bug-fix run scored unsolved on argument list, event count, exit code and stderr size. Its final message is line 14 of the stream. One Windows PC, Claude Code 2.1.284, subscription login; no new run. Your runs may differ.

      In the 39 runs where Claude Code refused a command, the exit code was 0 every time and is_error was true in none. Of those 39, 38 still passed the held-out tests. This was the one that did not.

      A script that checked only the exit code would have scored all 247 runs as fine, the 39 and the run that fixed nothing included. That is not a fault of the exit code. It says the process finished, and the process did finish. Whether the work got done is a different question.

      What to check, in this order

      1. The exit code. It tells you the process ended.
      2. The last line of the stream. In the result line, subtype should be success, is_error false and terminal_reason completed.
      3. The denials. Any permission_denied line, and any entry in permission_denials. A denied action was not performed, so some of the work may be undone.
      4. The final message. It is the agent's own account. Read it as a report, not as proof. In the run above it said exactly what happened, and the bug was still there.
      5. The work itself. Check the work itself: run the tests, or whatever check this job really needs. Nothing above replaces this step.

      What the docs say a non-zero exit means

      The saved headless page describes failing runs in these sentences. They are documented, not seen in these runs: no recorded run exited with a non-zero code.

      • "Claude Code exits with code 0 on success and a non-zero code when the run fails, so your scripts can branch on the exit status."
      • "When a failure happens inside the run, such as missing authentication, Claude Code prints the failure as the result on stdout."
      • "If you pass an invalid flag, Claude Code reports the error to stderr before the run starts."

      Quotedn = 0 recorded runs with a non-zero exit From Run Claude Code programmatically, saved 2026-10-10. The page also describes two more cases, documented and not run here: piped input over 10MB exits with a non-zero status, and a run stopped with SIGTERM exits with code 143.

      The second sentence matters to a script. When a run fails inside, the reason comes out on stdout as the result, so read that when the code is not 0, and do not rely on the code alone. The drill below runs six finished runs through six checks, in order. The first is the recorded run; the other five are constructed.

      Interactive · Gate Runnercandidate 1 of 6

      Which check stops this run?

      Six checks a script can run on a finished run, in order. For each of six runs, pick the check that stops it first; later checks are greyed out. If a run passes all six, pick the last button. The first run is recorded, the run that fixed nothing (the walkthrough run); the rest are constructed.

      Recorded, the run that fixed nothing (the walkthrough run): exit code 0, result subtype success, is_error false, one entry in permission_denials. No file changed and the visible test still failed.

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      MeasuredQuoted What is recorded and what is constructed
      Measured
      Six runs: the run that fixed nothing (the walkthrough run) is recorded; 5 are constructed. Constructed for this drill. Not a recorded run. Across all 247 runs every exit code was 0 and every result said success.
      Method
      A run stops at the first check it fails. The checks are our checklist. The vendor documents non-zero exit codes; no recorded run produced one.
      n
      6 runs. 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. No interval is claimed.
      Source
      The walkthrough stream; the run records; Run Claude Code programmatically (saved 2026-10-10).
      Excludes
      A recorded failing exit: none in 247 runs. Other setups. Whether six checks suffice for your job.
      Module six · 8 min

      Cost estimates and usage guards

      A headless run gives you two readings that are easy to mix up: a cost estimate the CLI prints, and a usage meter your plan keeps. This module takes the estimate first, then the meter, then a guard that reads it.

      The cost figure is an estimate

      The result line carries total_cost_usd. The CLI works it out on its own by pricing the run's tokens at list price. That makes it nominal: what those tokens would cost at the published price, not a bill. The docs call such figures client-side estimates that "can differ from your actual bill." They also say that with --output-format json "the response payload includes total_cost_usd and a per-model cost breakdown". That format was not run here. The result line of the recorded stream-json runs carried the same field.

      the 247 estimates added up to$6.94nominal, at list price
      median run$0.0243estimate at list price
      largest run$0.1202estimate at list price
      recomputed from token counts247 / 247matched within a millionth of a dollar

      List price, not spendCalculatedn = 247 runs The sum, median and largest of the CLI's total_cost_usd. Each run was recomputed from its token counts and the vendor's published list prices (from its pricing page, saved earlier). That checks the CLI's arithmetic, not any bill. One Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run.

      The runs used a subscription login and costBasis was list in all 247, so this page makes no claim about what any account paid. The drill below gives eight sentences about these figures. Decide which ones hold.

      Interactive · Decidercase 1 of 8

      Is this sentence true?

      Eight sentences about the cost figures in the recorded runs. Read PASS (the button says ship) as: true as written. Read FAIL (send back) as: false, or claims more than the figures show. Every dollar figure is the CLI's estimate at list price (nominal), not spend. The sentences are constructed; the figures are recorded.

      The 247 estimates add up to about seven dollars of spend.

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      MeasuredCalculatedList price, not spend Where each figure comes from
      Measured
      Eight constructed sentences. Constructed for this drill. Not a recorded run. Their figures are recorded or calculated over the 247 results, and every dollar figure is the CLI's estimate at list price (nominal), not spend.
      Method
      FAIL: the sentence calls an estimate spend or cost, or claims precision the vendor says is not there. Recomputed from token counts and published list prices, to within one millionth of a dollar.
      n
      8 sentences. 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. No interval is claimed.
      Source
      The run records; CLI reference and Run Claude Code programmatically (saved 2026-10-10).
      Excludes
      What any run was charged. Any plan meter.

      Caps: documented, not run here

      • --max-budget-usd: "Stop the run once estimated spend on API calls reaches this amount (print mode only)." The page adds that spend can pass the cap, so you should leave headroom, and that the cap is checked against the same client-side estimate.
      • --max-turns: "Exits with an error when the limit is reached. No limit by default." A turn is one round of the model answering. The installed help does not list this flag: it appears 0 times in both saved help outputs.

      Quotedn = 0 recorded runs used either flag From the CLI reference, saved 2026-10-10. Documented, not run here.

      The usage meter

      Cost is the tokens priced at list. The meter is separate: how much of your plan's allowance a rate-limit window has used. A rate-limit window is a period, such as five hours or seven days, in which your plan allows a share of usage. Every recorded stream carried lines about it: 250 rate_limit_event lines across the 247 streams, 1 to 3 per stream. In 249 of the 250, status was allowed and rateLimitType was five_hour.

      One line differed: status allowed_warning on the seven_day window, with a surpassedThreshold field. The harness script that made the runs read that line and stopped. It was started again later. This is the start of its own log line:

      STOPPED: usage limit signal: status 'allowed_warning', type 'seven_day'

      I publish no usage figure or reset time from that account. The script checked seven things after every run: an API key in use, fast mode on, overage in use (two settings that were off in all 247 runs), a rate-limit status other than allowed, an API error status, a missing result line, and a result that said is_error. It stopped at the first one and never retried. It also refused to start in two cases: inside a Claude Code session, with the message "this window is inside a Claude Code session (CLAUDECODE is set). Open a normal PowerShell window and run the script there.", and when an API-key variable was set, because its message says that can route runs to paid API usage. The docs agree that in -p mode "the key is always used when present".

      MeasuredQuotedn = 250 rate-limit lines Counted in the 247 saved streams. The log line and the start message are the harness script's own words. One line is one warning, so no rate is claimed.

      A usage guard

      A usage guard is a few lines of script that read the meter between jobs and stop before a limit. It matters most for a scheduler (cron, a service that runs a script at set times) or CI (a service that runs your scripts on every code change), because nobody is watching either. The guard script we run between build steps works like this:

      • It stops when the weekly reading is at or above a line, 75 percent by default.
      • A reading more than 90 minutes old counts as no reading. So does a missing reading, or one from a window that has already reset.
      • It exits with 0 to continue, 10 to stop, 11 for no reading and 12 for bad input.
      • A guard that cannot see the meter does not assume it is fine.

      Measuredn = 23 proof cases The script passed all 23, covering the paths that make no model call. They ran against the script, not against live meters. The drill below uses constructed readings only.

      Interactive · Instrumentlive

      What would the usage guard do?

      A usage guard is a few lines of script that read the meter between jobs and stop before a limit. This one follows the rules of the guard script we run between build steps: STOP at or above the line (75 percent by default); NO READING when there is no reading, it is more than 90 minutes old, or its window has reset. A reading that cannot be trusted is not treated as fine. Every reading here is constructed; none is from any account.

      Margin over the stop line
      —points

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      MeasuredReported What is measured and what is constructed
      Measured
      The guard script's rules: stop at 75 percent by default; no reading when none exists, it is over 90 minutes old or its window has reset; exit codes 0, 10 and 11. It passed 23 proof cases, covering the paths that make no model call. The readings are constructed. Constructed for this drill. Not a recorded run.
      Method
      The dial shows the margin over the line in points, negative meaning under, and is hidden in the NO READING band. BAD INPUT (exit code 12) is not modelled.
      n
      1 script, 23 proof cases. No usage figure from the recorded account is published.
      Source
      The guard script's source and proof run, read 2026-10-10. Real readings come from the rate-limit lines in all 247 recorded streams.
      Excludes
      Any real reading. Whether 75 percent suits your plan. The five-hour window (reported, not acted on).
      Module seven · 5 min

      The startup cost of a call

      Every claude -p call is a new process, and it has to start before it can work. In the 247 recorded runs the first line of the stream, system/init, arrived a median of 5.82 seconds after the process started. The fastest took 4.78 seconds and the slowest 8.17.

      Seconds before the first line arrived A histogram of when the first line of the stream arrived, in the 247 recorded runs. The fastest was 4.78 seconds, the median 5.82 seconds and the slowest 8.17 seconds. No run was faster than the fastest bar. Seconds before the first line arrived 247 runs: process start to the first stream line. fastest 4.78 s median 5.82 s slowest 8.17 s
      When the first line arrived. Each bar counts runs; the clay bar holds the median.
      first line arrives, median5.82 sof 247 runs
      one-line probe, start to exit7.42 smedian of three probes
      of which the result's duration_ms1.2 smedian of the same three
      time outside the result's own measured time6.16 smedian of 247 runs

      Calculatedn = 247 runs, and 3 probes Timed by the harness on one Windows PC. The probe prompt was "Reply with exactly: OK". The last tile is the harness's wall time minus the result line's duration_ms: time spent outside the run's own measured time. Every run passed --safe-mode, so these are start times with customizations off, and yours will differ.

      Each call pays that cost again. A job that reviews twelve files in twelve calls waits for twelve starts; one call that covers all twelve waits for one. Whether one prompt can do the whole job depends on the job, and I did not test whether a batched answer is as good. The drill does the arithmetic with your own start time.

      Interactive · Instrumentlive

      How much time goes on call overhead?

      A job reviews 12 files. How you split it into calls decides how many calls pay the overhead. Pick a split, the time one call spends outside its own measured time, and how many times the job runs; the readout is the total. The default, 6.16 seconds, is the median over the 247 recorded runs on one Windows PC of each run's wall time minus its result line's own measured duration: start-up and wind-down, outside the work. Replace it with your own: yours will differ.

      seconds
      times
      Time outside the calls' own measured time
      —seconds

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      CalculatedMeasured How the readout is calculated
      Measured
      Default 6.16 seconds: the median over 247 runs of wall time minus the result line's own measured duration, so it includes start-up and some wind-down. The first line of a stream arrived a median 5.82 seconds after the process started (4.78 fastest, 8.17 slowest); the one-line probe took a median 7.42 seconds, 1.2 of them the result line's own measured duration (3 probes).
      Method
      Calculated: calls x time per call x runs. The 12 files and five splits are constructed. It counts overhead only; whether a batched answer is as good was not tested.
      n
      247 runs, 3 probes. 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. Per call, one PC.
      Source
      The run records; Run Claude Code programmatically (bare mode: documented as reducing startup time, not run here).
      Excludes
      Your own overhead: measure it. Bare mode. Other machines or days.

      Bare mode: documented, not run here

      The docs offer a faster start for scripts: "Add --bare to reduce startup time by skipping auto-discovery of hooks, skills, custom commands, subagents, installed plugins, MCP servers, auto memory, and CLAUDE.md." They call it "the recommended mode for scripted and SDK calls" that "will become the default for -p in a future release". They also say to set an API key "because bare mode doesn't use your subscription login".

      All 247 recorded runs used a subscription login and none passed --bare, so nothing about it was measured, its start time included. The runs used --safe-mode instead. The docs describe it as "Start with all customizations disabled to troubleshoot a broken configuration" and say "Authentication, model selection, built-in tools, and permissions work normally", unlike --bare.

      Quotedn = 0 recorded runs used --bare From the headless page and the CLI reference, saved 2026-10-10. The docs describe versions up to 2.1.296; the runs used 2.1.284.

      Module eight · 6 min

      Practice kit

      The kit is two programs you can run yourself: a checker for a saved stream, and a wrapper that runs claude -p and then the checker. The block below carries the downloads, the checks, an example of each command and what the checker said about the 247 recorded streams.

      Downloads need JavaScript and this block's script file.

      What the checker looks at

      check_stream.py reads one saved stream and prints PASS, FAIL, WARN or INFO for each of eight checks.

      1. The stream starts with system/init and ends with result.
      2. The result line says subtype success and is_error false.
      3. terminal_reason is completed.
      4. Nothing was denied: no permission_denied line, and an empty permission_denials. A denied action did not run, so the work may be undone.
      5. The model. With --expect-model, the init line, every assistant line and every modelUsage key must name that model. Without it, the names are only printed.
      6. With --require-subscription, apiKeySource is none.
      7. The last rate-limit line says allowed. Only the status word is read; no usage figure or reset time is printed.
      8. The CLI's cost estimate, printed and labelled an estimate, not a bill.

      A WARN means a check could not be made. That is no reading, not a good reading. It changes the exit code only with --strict.

      How to run them

      python check_stream.py run.jsonl --expect-model <model name> --require-subscription
      python run_headless.py --prompt-file prompt.txt --out-dir runs -- --model <model name> --tools Read --permission-prompts none

      The first checks a stream you saved. The second runs claude -p once with your flags after the --: --tools Read limits the run to the Read tool, and --permission-prompts none denies anything that would ask a person. It saves what it printed, then runs the first on it.

      The wrapper has only run against a stub. I tested run_headless.py against stub programs that replay a recorded stream. I never ran it against the real claude program or on macOS or Linux. Try it first on a harmless prompt.

      What it said about the recorded streams

      I ran check_stream.py once on each of the 247 saved streams of the recorded runs (one Windows PC, Claude Code 2.1.284, a subscription login), with --require-subscription and --expect-model set to each run's own model name. I made no new run.

      CheckWhat it asksStreams it failed, of 247
      aFirst line system/init, last line result0
      bsubtype success and is_error false0
      cterminal_reason completed0
      dNo denied action39
      eEvery model name equals the run's own --model0
      fapiKeySource is none0
      gThe last rate-limit line says allowed1

      Measuredn = 247 recorded streams Streams each check failed, read from the saved files by a program we wrote later.

      Check d failed on the 39 streams in which a command was denied. Check g failed on the one stream whose only rate-limit line said allowed_warning. Claude Code itself exited 0 on all of these runs, so a script that read only its exit code would have passed them. The run that fixed nothing is one of the 39, because it had one denial. The checker fails it for the denial and not because it looked at the repository. A run that fixed nothing and had no denial would pass every check here, so this checker does not replace running the tests.

      Measured How the checker was tested
      Measured
      How many of the 247 saved streams each check failed, and whether the checker and wrapper do what the page says when their input is damaged.
      Method
      One checker process per stream, with that run's own model name and --require-subscription. Every outcome was compared with a separately written second reading of the same file and with the run's saved record. Copies of one recorded stream were then damaged one way at a time (last line dropped, model name changed, denial added, error flag set, API-key source flipped, last rate-limit status set to rejected, empty file, a line that is not JSON); the check named for that damage had to fail, or the checker had to refuse the file. The wrapper ran only against stub programs. Python 3.14, 3.10 and 3.11 printed the same.
      n
      247 saved streams, one checker run on each. No confidence interval is claimed.
      Source
      The saved streams and run records of the 247 recorded runs (one Windows PC, Claude Code 2.1.284, a subscription login). No new run was made.
      Excludes
      Streams from any other setup. A wrong fix that ran without a denial. The real claude program, which the wrapper never met. macOS, Linux, Python 3.9 (only parsed, not run), and the wrapper on any Python but 3.14.

      Before the first unattended run

      Settle where the run starts. The folder a run starts in is its run folder, also called its working directory. The docs say "A -p session shows no workspace trust dialog and no per-server approval prompt." They also say that without --bare it runs the hooks in a project's settings file and connects the servers in its MCP file, even in a folder you have not trusted. So run an unattended job in a folder you control.

      The recorded runs also passed --no-session-persistence, which the docs describe as "Disable session persistence so sessions are not saved to disk and cannot be resumed." The 75 bug-fix runs passed --restricted: "Use it when an evaluation harness drives claude on a shared machine and Claude Code must not run commands or read that machine's user and project settings." Those runs named Bash in --tools, which the docs say keeps it available. Give your own job a time limit, because a -p run can stay open to wait for background work, and the default cap is 10 minutes of continuous idle waiting. Switches that turn the permission checks off, such as --dangerously-skip-permissions and the bypassPermissions mode, were not used in any recorded run, and this page does not recommend them.

      Quotedn = 247 recorded runs Sentences from the headless page and the CLI reference, saved 2026-10-10. Documented, not run here. The harness script refused to start inside a Claude Code session (module six).

      The drill asks four questions and lists the steps that apply to you.

      Interactive · Setup Checkeryour path

      Before the first unattended run

      Four questions; steps that do not apply disappear. Each step names its source: our harness script, which ran the 247 recorded runs, or a saved vendor page. Nothing here was run on macOS or Linux, and we did not run the practice kit's wrapper against the real CLI.

      1. Open a normal terminal window firstOur harness script refused to start inside a Claude Code session. Its message said: this window is inside a Claude Code session (CLAUDECODE is set). Open a normal PowerShell window and run the script there. Do the same: open your own terminal and start the job there. The practice kit's wrapper refuses too. (Source: our harness script.)
      2. Decide on purpose about the API-key variableOur recorded runs used the subscription login (apiKeySource said none in 247 of 247 runs), and our harness stopped if an API key was in use. The vendor's authentication page says: "In non-interactive mode (-p), the key is always used when present." So a key left in the environment is used by a headless run. Unset it (unset ANTHROPIC_API_KEY falls back to the subscription) or decide on purpose. The practice kit's wrapper refuses to start while the variable is set. (Sources: our harness, saved Authentication page.)
      3. Check your version and the flags you plan to useRun claude --version and claude --help. The 13 vendor pages saved for this class mention versions up to 2.1.296; the runs used 2.1.284. Of the 74 flags in the CLI reference, 15 were not in that help, --max-turns among them (0 mentions in the saved help). Documented is not tested: check each flag in your own help. (Sources: saved CLI reference, saved help output.)
      4. Update before you rely on the permission-prompts flagThe vendor says --permission-prompts needs 2.1.259 or later; earlier versions reject it with an unknown-option error. The runs used 2.1.284. (Source: saved headless page.)
      5. Give the job a folder you controlWithout --bare, a -p session runs the hooks in a project's settings file and connects the servers in its MCP file, even in a folder you have not trusted, with no trust dialog. Our runs passed --safe-mode and --strict-mcp-config. Use an empty or known folder. (Sources: saved headless page, CLI reference.)
      6. Turn permission prompts off and say what may runNobody is there to answer: pass --permission-prompts none. The vendor's headless page says to when nobody can answer, for example in a scheduled job; anything that would need approval and is not covered by your allow rules is denied, and the run continues. Write the allow rules in the job spec above. (Source: saved headless page.)
      7. Read the vendor's CI pages before you copy anythingThe saved GitHub Actions and GitLab pages describe running Claude Code in a pipeline; we ran neither. The vendor calls --bare the recommended mode for scripted calls, but bare mode does not use your subscription login, so it needs an API key. We ran no bare run. (Sources: saved CI and headless pages.)
      8. Set a time limitPut a timeout around the process in your scheduler or wrapper. Our harness had a limit and stopped none of the 247 runs for passing it. Stopping a run with SIGTERM makes Claude Code exit with code 143 and record no result for the unfinished turn, the vendor documents; we did not test that. (Sources: our harness, saved headless page.)
      9. Save the stream, stderr and the exit code of every runWrite the stream to a file, keep stderr apart, and record the exit code. Our harness kept all three for each of the 247 runs, which is how this class counted them. The exit code alone would not have shown the 39 runs with a denial. (Source: our harness script.)

      Your answers stay in this browser. Nothing is sent anywhere. Reset clears them.

      MeasuredQuotedn = 247 recorded runs Counts measured from 247 recorded claude -p runs, one at a time on one Windows PC, Claude Code 2.1.284, subscription login (no API key); no new run was made. Each step names its source. The vendor's behaviour is documented, not run here, and the kit wrapper was not run against the real CLI.

      Module nine · 1 min

      Where to go next

      One more class is planned, with no date and no promise that it will appear: What Your Coding Agent Is Allowed to Run.

      Module ten · 3 min

      What we did not verify

      This is the page's own list of gaps. I wrote it from saved runs and made no new run, so each item is a limit on those files or on what we tested.

      1. No new run was made. Every figure comes from 247 saved runs, recorded one at a time earlier.
      2. One PC, one CLI version. The runs came from one Windows PC with Claude Code 2.1.284 and a subscription login, while the 13 saved docs pages describe versions up to 2.1.296.
      3. No non-zero exit was recorded. What a failing run prints and exits with is documented here, not seen.
      4. Several flags were not run. That covers --output-format json, --json-schema, --max-turns, --max-budget-usd, --bare, --resume, --continue and --input-format.
      5. Other permission modes were not run. No recorded run passed --permission-mode, so dontAsk and bypassPermissions among its values were not tried.
      6. SIGTERM and exit code 143 were not tried. The 10MB input cap and the 10-minute background wait are documented only.
      7. The installed help does not list --max-turns. The docs describe it, so I cannot say whether your version accepts it.
      8. The wrapper only met a stub. We did not run run_headless.py against the real claude program.
      9. The checker read streams from one harness. It cannot see a wrong fix that ran without a denial, so it does not replace running the tests.
      10. macOS, Linux and CI services were not run. The kit was tested on Windows only. The counts above come from Python 3.14; the checker also printed the same on 3.10 and 3.11, and 3.9 was only parsed, not run.
      11. Cost figures are estimates. I read no bill, so nothing here says what any account paid.
      12. The usage guard met no live meter on this page. The drill's readings are constructed, and the 23 proof cases ran against the script.
      13. The drills' constructed cases are not recorded runs. Each drill says which of its cases are recorded.
      14. The docs may have changed since 2026-10-10, when the pages were saved.
      15. Your plan's rules for headless use were not tested. Check them for your kind of job.
      16. The walkthrough is one run. It shows one way a run can go. It does not show how often that happens.

      Bottom line

      Run it with nobody watching, then read the stream, check the result line and the denials, read the model back, and check the work itself. In 247 recorded claude -p runs on one Windows PC (Claude Code 2.1.284, a subscription login, one at a time) every exit code was 0, stderr was empty and every result line said success, even in the 39 runs where a command was refused and the one bug-fix run that left its repository unchanged. The model named in the stream matched the flag in 247 of 247 runs, and the first line took a median of 5.82 seconds to arrive. This page shows no failing exit, no run of --bare, --max-turns, --max-budget-usd or JSON output, and no run of the wrapper against the real program; I wrote it from saved runs with no new run made.

      Questions

      How do I run Claude Code headless?

      Add the -p flag: claude -p reads a prompt, does the work with nobody at the keyboard, prints what happened and exits. The 247 recorded runs on this page also passed --output-format stream-json with --verbose, a --tools list and --permission-prompts none, and took the prompt on standard input. One Windows PC, Claude Code 2.1.284, a subscription login; no new run was made. Check each flag against your own version.

      What does claude -p do?

      It is print mode. Claude Code takes your prompt, does the work as one fresh process, writes the result to standard output and exits with a code. With --output-format stream-json each step of the run is one JSON line, from a system/init line first to a result line last. That held for all 247 recorded streams, on one Windows PC with Claude Code 2.1.284.

      Does exit code 0 mean the headless job worked?

      Not by itself. It means the process finished. All 247 recorded runs exited 0 with an empty stderr and a success result line, including 39 runs in which Claude Code refused a command and the one bug-fix run that left its repository unchanged. Read the result line and the denials, then check the work itself, for example by running the tests. The docs describe non-zero exits; none was recorded here.

      How do I know which model answered a headless run?

      Read it from the stream instead of trusting the flag. Four places name the model: the system/init line, every assistant line, the result line's modelUsage key and its canonicalModel field. In the 247 recorded runs all four matched the --model value, and every run passed a full model name, not an alias. Log those names next to each run's result.

      How do I stop a headless run asking for permission?

      Nobody can answer, so say what may run. Pass --permission-prompts none, which the docs say needs Claude Code 2.1.259 or later, and list allow rules such as Bash(python -m pytest *). The docs add that in a -p run with no host these requests are denied either way. All 247 recorded runs passed the flag, so they cannot show what it changes. Switching the checks off was used in none, and this page does not recommend it.

      Why was a command denied in a headless run?

      A command that needs approval and no allow rule covers is denied at once when nobody can answer; plain read-only commands ran without a rule. The recorded runs had 41 denials in 39 runs, all on Bash, each with the same reason: no approval surface in this session. A rule has to match each part of a chained command, so one unapproved part such as sed -i refused the whole line. Denials show in the stream and in permission_denials.

      Does a headless Claude Code run cost money?

      The CLI prints total_cost_usd, its own estimate at list price. Across the 247 recorded runs the estimates added up to 6.94 dollars, nominal, with a median of 0.0243 and a largest of 0.1202 dollars per run. The docs say such estimates can differ from your actual bill, so this page makes no claim about what anyone paid. The runs used a subscription login and no API key.

      How do I keep a scheduled job from running into a usage limit?

      Read the meter between jobs and stop before the limit. All 247 recorded streams carried rate-limit lines, and one carried an allowed_warning on the seven-day window, which stopped the harness script. A guard should treat a missing, stale or already-reset reading as no reading, not as fine. The guard script we run between build steps stops at 75 percent by default and counts a reading over 90 minutes old as no reading.

      Why did the recorded runs take about six seconds to print a first line?

      Each recorded call was a new process that had to start before the model worked. In the 247 recorded runs the first stream line arrived a median of 5.82 seconds after the process started (fastest 4.78, slowest 8.17) with --safe-mode on, on one Windows PC. A one-line probe took a median of 7.42 seconds in all, 1.2 of them the result line's duration_ms. Where one prompt can cover the job, fewer calls pay fewer starts.

      Does --bare work with a subscription login?

      The docs say no: bare mode doesn't use your subscription login, so it needs other credentials, such as an API key. They say --bare skips auto-discovery of hooks, skills, MCP servers and CLAUDE.md so scripted calls start faster. That is documented, not run here: no recorded run used --bare, and all 247 used a subscription login with --safe-mode instead.

      What is stream-json?

      It is an --output-format value that prints newline-delimited JSON, one object per line, as the run goes. The 247 recorded streams held 13,033 lines of nine kinds. Every stream began with a system/init line and ended with a result line. The other values, json and text, are documented, not run here.

      Is the class free, and do I need to sign up?

      Yes, it is free. You do not need to sign up or give an email address. The practice kit downloads (a stream checker, a wrapper, a README and a license notice) are built in your browser from the page, with no server involved. The wrapper has only run against a stub, not the real claude program.