Blog / claude code
claude -p exited 0 but did nothing? Check what it was refused
A blocked tool call doesn't fail a claude -p run: the exit code is 0. Read permission_denials and is_error in the JSON result instead. Script included, tested.
On this page
- What the exit code tells you
- See what was refused
- A wrapper that fails loudly
- Let it finish what it was refused
- Max turns and budget
- When the yes is someone else's
- Frequently asked questions
- Why does claude -p exit 0 when a command was blocked?
- How do I check if a claude -p run failed?
- How do I see which commands Claude Code wanted to run?
- Can I approve a refused command and continue the same run?
- Try it with your team
Your nightly claude -p job ran, the log says it finished, the exit code is 0, and nothing changed. Most of the time the agent wanted to run a command, wasn't allowed to, and said so politely in its last message. From the outside that looks exactly like success.
We tested what claude -p returns in each case with Claude Code 2.1.318, and wrote a small wrapper that tells them apart.
What the exit code tells you#
| What happened | Exit code | subtype | is_ |
|---|---|---|---|
| Did the job | 0 | success | false |
| A tool call was refused | 0 | success | false |
Hit --max-turns | 1 | error_ | true |
| API error (bad model, 404) | 1 | success | true |
Two things to take from it:
- A refused tool call is not an error. The run completes normally, and the only trace is the agent's own sentence ("the
rm notes.txtcommand needs your approval…") and a list in the JSON. - Don't check
subtypealone. With a model name that doesn't exist, the run exited 1 withis_, yeterror: true subtypestill readsuccess. Checkis_.error
See what was refused#
Ask for JSON and read permission_:
echo hello > notes.txt
claude -p "Delete notes.txt using rm." --output-format json > result.json
echo "exit: $?"
jq '{is_error, permission_denials}' result.jsonexit: 0{
"is_error": false,
"permission_denials": [
{
"tool_name": "Bash",
"tool_input": { "command": "rm notes.txt", "description": "Delete notes.txt" }
}
]
}(We trimmed the tool_ field.) The file was still there.
A wrapper that fails loudly#
Save this as run-claude.sh. It prints the answer, exits 1 on an error and 2 when something was refused, and says which command it was:
#!/bin/sh
# Usage: ./run-claude.sh "prompt" [more claude flags]
out=$(claude -p "$@" --output-format json < /dev/null)
code=$?
if [ -z "$out" ]; then echo "claude printed nothing (exit $code)" >&2; exit 1; fi
json() { printf '%s\n' "$out" | jq -r "$@"; }
json '.result // empty'
if [ "$code" -ne 0 ] || [ "$(json '.is_error')" = "true" ]; then
echo "FAILED: $(json '[.subtype, .terminal_reason, (.api_error_status // empty | tostring)] | join(" ")')" >&2
exit 1
fi
if [ "$(json '.permission_denials | length')" -gt 0 ]; then
echo "BLOCKED: $(json '[.permission_denials[] | .tool_name + ": " + (.tool_input.command // .tool_input.file_path // "")] | join("; ")')" >&2
exit 2
fichmod +x run-claude.sh
./run-claude.sh "Delete notes.txt using rm."; echo "exit=$?"I couldn't delete `notes.txt` because the `rm` command needs your approval and wasn't run. Approve it and I'll run it again, or run `rm notes.txt` yourself.
BLOCKED: Bash: ls -la notes.txt && rm notes.txt && ls notes.txt
exit=2The agent's wording changes from run to run, and so can the exact command (this time it wrapped rm in two ls checks). The BLOCKED line shows you what it actually tried.
The other cases, as we ran them:
$ ./run-claude.sh "Reply with the single word ok."
ok (exit 0)
$ ./run-claude.sh "List every file here, then read each one, then summarize." --max-turns 1
FAILED: error_max_turns max_turns (exit 1)
$ ./run-claude.sh "Say ok." --model no-such-model
There's an issue with the selected model (no-such-model). It may not exist or you may not have access to it.
FAILED: success api_error 404 (exit 1)File edits are refused the same way. Without edit permission, a request to create a file comes back as a Write denial with the path:
$ ./run-claude.sh "Create hello.txt containing the word hi."
I couldn't create `hello.txt`. The write to `/home/you/project/hello.txt` was blocked because permission hasn't been granted.
BLOCKED: Write: /home/you/project/hello.txtIf a job is meant to change files, run it with --permission-mode acceptEdits, or allow the tools it needs with --allowedTools. If it isn't, a Write or Edit denial is a sign the prompt asked for more than you thought.
Three details in the script matter:
< /. Without it, a run with no piped input waits for stdin and printsdev/ null Warning: no stdin data received in 3s, proceeding without it. In a script there is nothing to wait for. Our post on piped input that Claude ignored covers the other side of that warning.printf, notecho. Undershon many Linux systems,echoturns the\ninside the JSON into real line breaks andjqthen fails to parse it. Our first version had exactly that bug;printf '%s\n'passes the text through unchanged.- The prompt goes first. Flags like
--allowedToolstake several values, so a prompt written after them is read as one more tool name. Keep the prompt right after-p.
Let it finish what it was refused#
Every result carries a session_. Once you've decided the command is fine, resume that same session with just that command allowed:
claude -p "Approved. Go ahead." --resume "$(jq -r .session_id result.json)" \
--allowedTools 'Bash(rm notes.txt)'It ran rm notes.txt and the file was gone. Allowing the exact command, rather than all of Bash(rm:*), keeps the next run as tight as this one.
If the same command is refused every night, it belongs in the job's allow list (or in .claude/). Our post on running Claude Code on a schedule shows where.
Max turns and budget#
--max-turns and --max-budget-usd stop a run that goes on too long, and both show up as is_. Setting them is covered in limiting an unattended Claude Code run; the wrapper above already reports them.
When the yes is someone else's#
A refused command in a scheduled job usually waits until somebody reads the log. With Joxo, an agent's question reaches the person who can answer it on their phone or desktop, and the answer goes back into the session. See approving agent actions from your phone.
Frequently asked questions#
Why does claude -p exit 0 when a command was blocked?#
A refused tool call isn't an error: the agent carries on, explains that it needs approval, and the run completes normally. Use --output-format json and check that permission_denials is empty.
How do I check if a claude -p run failed?#
Use --output-format json and check is_error, plus the exit code. Don't rely on subtype alone: in our test an API error exited 1 with is_error true but subtype still "success".
How do I see which commands Claude Code wanted to run?#
Read permission_denials in the JSON result. Each entry has the tool name and its input, such as the Bash command.
Can I approve a refused command and continue the same run?#
Yes. Resume the session by its session_id with the command allowed: claude -p "Approved. Go ahead." --resume <id> --allowedTools 'Bash(rm notes.txt)'. Put the prompt before --allowedTools.