Headless mode and JSON output
Everything so far has been a conversation. -p makes it a command, and a command can go in a script.
You have been using it since lesson 1. It runs one prompt, prints the answer and exits. The version that matters for automation adds two flags.
claude -p "The demo shows a bug: after removing a link, the next code collides with an
existing one and overwrites it. Fix next_code in shorten.py so a code is never reused." \
--allowedTools "Read,Edit,Bash(python3 demo.py)" \
--output-format json{
"type": "result",
"subtype": "success",
"is_error": false,
"num_turns": 4,
"duration_ms": 17232,
"total_cost_usd": 0.0233022,
"result": "Done. `_next_index` tracks total codes generated (only increments), not current link count. Removed links no longer cause collisions.",
"permission_denials": []
}That is a real run against the project from lesson 0, and it fixed the bug. Four turns, seventeen seconds, two and a third cents, and nothing refused. The two flags are what made it possible: --allowedTools answered the permission questions in advance, because there was nobody there to answer them, and --output-format json turned the whole run into one object a script can read.
What it changed
def next_code():
- n = store.count()
- return ALPHABET[n % 26] + str(n // 26)
+ global _next_index
+ code = ALPHABET[_next_index % 26] + str(_next_index // 26)
+ _next_index += 1
+ return codeThe count of links stored was the wrong thing to count, because deleting one made it go backwards. Counting codes handed out instead never goes backwards, so a code is never reused.
python3 demo.pyfirst : a0 -> https://krishnaik.in/courses second: b0 -> https://krishnaik.in/blog third : c0 -> https://krishnaik.in/about second: b0 -> https://krishnaik.in/blog
The third link is c0 now, and the second one still points where it always did. Compare that with lesson 0, where the last two lines were the same URL.
What comes back
{
"type": "result",
"subtype": "success",
"is_error": false,
"num_turns": 2,
"duration_ms": 8402,
"total_cost_usd": 0.008885,
"result": "Waiting for write permission to create README.md.",
"permission_denials": [
{
"tool_name": "Write",
"tool_use_id": "toolu_01WT",
"tool_input": {
"file_path": "/tmp/linkshort/README.md",
"content": "# Link shortener\n"
}
}
],
"session_id": "eebc5636-3afb-47d4-a4f7-da64a313841d"
}This is the real object from lesson 4, where the write was refused. Everything a script needs is in it: whether it failed, how many turns it took, what it cost, the answer as text, and a list of anything permission stopped.
Reading it in a pipeline
The panel beside this is a script that reads that object and decides whether the run should be treated as a success. Press Run.
"""Read a headless run the way a script would.
`claude -p ... --output-format json` prints one object like this.
A pipeline cares about three things: did it work, what did it
cost, and was anything refused.
"""
import json
run = json.load(open("run.json"))
print("ok: ", not run["is_error"])
print("turns: ", run["num_turns"])
print("cost: ", f"${run['total_cost_usd']:.4f}")
print("answer: ", run["result"])
for denial in run.get("permission_denials", []):
tool = denial["tool_name"]
print("refused:", tool, "->", denial["tool_input"].get("file_path"))
status = 1 if run["is_error"] or run.get("permission_denials") else 0
print("exit: ", status)The last line is the point. A script ends with raise SystemExit(status), so a run where something was refused exits non-zero and a CI job fails loudly rather than carrying on with work that did not happen. A denial is not an error to Claude Code, and turning it into one is your decision to make.
--output-format stream-json gives you the same information as it happens, one event per line, which is what lesson 2 used to show the loop. Use it when you want to watch a long run rather than wait for it.One thing to know before you script it: sessions started with -p stay out of the session picker and out of --continue, as lesson 9 said. Keep the session id from the JSON if you want to reopen one.
- Change
is_errorto true in the panel and watch the exit code change. - Run a real headless prompt in a scratch repository with
--output-format json.
Slow is fine. Stopping is the only problem.