Script Claude Code with claude -p
Everything so far had you at the keyboard. claude -p is the same engine with the interface removed — one prompt in, one answer out, one exit code. Scripts and CI start here.
01The goal
Write a script that runs Claude Code unattended, reads its JSON result and exit code, and limits the run to the tools and turns the job needs.
02The idea
claude -p "<prompt>" runs one prompt without the interactive interface, prints the answer, and exits 0 — or non-zero when the run fails. It reads stdin, so a script can pipe a diff in alongside the prompt.
With --output-format json you get one JSON object instead of prose: result holds the answer; is_error says whether the run failed inside; num_turns and session_id describe the run; permission_denials lists every tool call that was refused. A subtype field distinguishes success from the ways a run stops early — error_max_turns among them — and a run that stops at a limit has no result at all. Check the exit code and is_error before you trust result.
Nobody is there to answer a permission prompt, so pass a mode every time — a run where nothing sets one can start in auto mode. dontAsk is the mode built for this: it denies every call that would otherwise prompt, while reads, read-only commands, and anything your allow rules name still run. Two flags complete the fence: --allowedTools names what may run anyway, and --max-turns caps the turns. (--max-turns counts only the turns that call tools.)
03The walkthrough
- 01Ask one question and save the JSON: claude -p "In one sentence, what does this repository do?" --output-format json --permission-mode dontAsk > first.json — then echo $? for the exit code, and read the fields with jq. Reads in the working directory need no approval, so dontAsk let Claude look around freely.
- 02Ask for something that needs approval: "Run the test suite and say how many tests pass", same flags plus --setting-sources user so the run skips the project’s settings files — its allow rules and its sandbox auto-allow cannot approve the test command now. The run finishes, but permission_denials lists the refused Bash call and result explains that the tests could not run.
- 03Allow exactly that command, and cap the turns: add --allowedTools "Bash(npm test *)" --max-turns 3. This time subtype is success and the denials list is empty. The space before the * matters — and the trailing " *" also matches the bare command. A pipeline such as npm test 2>&1 | tail -20 passes too: each part is checked on its own, and tail is one of the read-only commands that never prompt.
- 04Hit the cap on purpose: a task that needs read-then-run in sequence, with --max-turns 1 and --allowedTools "Read,Bash(npm test *)". The exit code is non-zero, subtype is error_max_turns, and there is no result field. That is what a stopped run looks like — decide now how your script will treat one.
04Your turn
Write scripts/review-staged.sh — a review you can run before every commit. It pipes git diff --cached into one claude -p call, with the prompt right after -p; passes --output-format json, --permission-mode dontAsk, an --allowedTools list of exactly Read, Grep and Glob (on macOS, Linux and WSL, naming either Grep or Glob in the list brings both back), and a --max-turns between 1 and 10; saves the JSON to the path in its first argument; prints the result text; exits non-zero when claude does or is_error is true; and when nothing is staged, says so and exits non-zero without calling Claude at all. Commit it, stage a one-line change, run it once, and read the review before you commit that change. The capstone reuses this script.
Three unattended-run edges to keep. A -p run shows no trust dialog, but it still runs the hooks in a project’s .claude/settings.json and connects the servers in its .mcp.json — even in a folder you have never trusted. Before you script Claude Code in a repository you did not write, pass --setting-sources user. When the ANTHROPIC_API_KEY environment variable is set, a -p run always uses that key instead of your plan. And total_cost_usd in the JSON is a client-side estimate, not your bill.
What does --permission-mode dontAsk do with a call that would normally prompt?
- AIt queues it for when you return
- BIt denies it — reads, read-only commands and allow-rule matches still run
- CIt approves it for the session
→Check your answer
dontAsk auto-denies anything that would prompt. Reads and read-only commands never prompt, and --allowedTools entries are pre-approved — that trio is the whole least-privilege toolkit for unattended runs.
A run stops at --max-turns. What does the JSON look like?
- Asubtype success with a short result
- Bsubtype error_max_turns, and no result field
- Cis_error true and result null
→Check your answer
A capped run stops early with subtype error_max_turns and produces no result — one more reason to check subtype before trusting result.
Why pass --setting-sources user before scripting inside a repo you did not write?
- AIt skips the project’s settings files — hooks, allow rules, .mcp.json servers
- BIt switches to API-key billing
- CIt disables the sandbox
→Check your answer
A -p run executes a repository’s hooks and MCP servers even in a never-trusted folder. --setting-sources user leaves the project’s settings out of the run — and its deny rules out too, so pass those you need on the command line.