Lesson 27 of 32
Non-Interactive Mode
Running Claude Code with nobody at the keyboard — the print flag, output formats a script can parse, fanning out across hundreds of files, and scoping permissions for an unattended run.
Every lesson so far has assumed a person at the keyboard: you type, Claude works, you watch, you correct. That is the right way to learn the tool and it is still how most of your work will happen.
This lesson is about the other mode. Claude Code can run with nobody watching — inside a script, a pre-commit hook, a CI job, or a loop over two thousand files. The mechanics are simple. The judgement about what to trust it with unattended is the part that takes care.
The flag
Add -p — the long form is --print — and Claude Code runs the prompt without
an interactive session, prints the answer, and exits.
claude -p "What does the auth module do?"That is the whole idea. It exits with code 0 on success and a non-zero code on failure, which is what lets a script branch on the outcome the same way it would for any other command.
Everything you know about prompting still applies. A vague prompt produces a vague answer whether or not a human is watching; the difference is that here nobody is around to notice and redirect it.
What comes back
By default you get plain text, which is fine for reading and useless for parsing. Two other formats exist, and the choice depends on whether you want one answer at the end or a running commentary.
text
The default. Just the response, as prose. Right for a human reading a terminal or a quick check in a shell script.
json
One JSON object containing the text in a result field, plus metadata about the run — the session ID, usage, and a cost estimate. Right when a script needs to pull one value out.
stream-json
One JSON object per line, emitted as the run proceeds, ending with a result message carrying the final text. Right when you want to display or log progress rather than wait for the end.
You select one with --output-format. The stream-json form also needs
--verbose, and --include-partial-messages if you want text as it is
generated rather than a message at a time.
claude -p "Summarise this project" --output-format json | jq -r '.result'jq is a small command-line tool for pulling fields out of JSON. It is not
part of Claude Code, but you will see it in nearly every example, because a
JSON object you cannot easily reach into is not much better than prose.
Piping data in and out
Non-interactive mode reads standard input, which means it behaves like an ordinary Unix command and composes with the rest of your shell.
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txtThat is a genuinely useful thing to have in a build script. So is this, which turns Claude into a project-specific linter that only ever looks at what changed:
{
"scripts": {
"lint:claude": "git diff main | claude -p \"you are a typo linter. for each typo in this diff, report filename:line on one line and the issue on the next. return nothing else.\""
}
}Note what the pipe buys you beyond convenience: because the diff arrives on
standard input, Claude does not need permission to run git diff itself. You
have narrowed what the run can do simply by handing it the data instead of the
ability to fetch it. Piped input is capped at 10MB, so for anything larger,
write it to a file and name the path in the prompt.
Fanning out across many files
This is the pattern that makes non-interactive mode worth learning. A large mechanical migration — two thousand files that all need the same change — is miserable by hand and awkward in one long session, because the context window fills long before the work is done.
Instead, run a separate short-lived Claude for each file.
- Step 1
Produce the list
Ask Claude, in a normal session, to write the files that need changing to a file such as files.txt.
- Step 2
Write the loop
A shell loop that calls claude -p once per file, with the file path interpolated into the prompt.
- Step 3
Refine on three
Run it against the first two or three files and read the diffs. Fix the prompt where it went wrong.
- Step 4
Run the set
Only now let it loose on the full list, with tools scoped to what the task actually needs.
for file in $(cat files.txt); do
claude -p "Migrate $file from React to Vue. Return OK or FAIL." \
--allowedTools "Edit" "Bash(git commit *)"
doneEach invocation is a fresh context holding one file, which is both cheaper and
more reliable than one enormous session. Asking for OK or FAIL gives you
something greppable at the end rather than two thousand paragraphs.
Permissions when nobody is watching
Interactively, permissions are a safety net you barely notice: Claude tries
something unexpected, you get a prompt, you say no. Unattended, that net is
gone. Whatever you allowed is what the run can do, and nothing will stop it
part-way to check. The same goes for the confirmation you normally get when
opening an unfamiliar codebase: there is nobody to ask, so it is skipped, and a
-p run in a repository you have not read is trusting it by default.
--allowedTools takes the same rule syntax you met in the permissions lesson,
so you can be specific rather than opening up whole tools:
claude -p "Look at my staged changes and create an appropriate commit" \
--allowedTools "Bash(git diff *)" "Bash(git status *)" "Bash(git commit *)"The trailing * is prefix matching, and the space before it matters:
Bash(git diff *) matches commands starting with git diff, whereas
Bash(git diff*) would also match git diff-index. This is exactly the kind
of detail that is invisible until it is not.
Auto mode — where a classifier reviews actions instead of prompting you — also
works with -p, and is what you want for an unattended run. It behaves
differently here for an obvious reason: if the classifier keeps blocking what Claude is trying
to do, there is no user to fall back to, so the run aborts. That is the right
outcome. A pipeline that fails is a problem you will find; a pipeline that
quietly did half the work is one you will not.
A session, refined
You typed: claude -p "Migrate src/components/Badge.tsx from the old Button props API to the new one. Return OK or FAIL." --allowedTools "Edit"
Output: OKWhyOne file, one invocation, one word back. Before trusting that, read the diff — 'OK' is Claude's opinion of its own work, which is exactly the thing this course keeps telling you not to accept on its own.
You typed: git diff src/components/Badge.tsx
Output: - <Button type="primary" size="sm"> + <Button variant="primary" size="small"> - import Button from '../ui/Button' + import { Button } from '../ui/Button'WhyThe prop rename is right. The import change was not asked for, and it is wrong for this project. On three files that is a two-minute fix to the prompt. On two thousand it is a bad afternoon.
You typed: claude -p "Migrate src/components/Badge.tsx from the old Button props API to the new one. Change only JSX props on Button. Do not touch imports. Return OK or FAIL." --allowedTools "Edit"
Output: OKWhyThe instruction that was missing is a negative one. Vague prompts drift towards tidying, and tidying across two thousand files is a diff nobody can review.
You typed: for file in $(cat files.txt); do claude -p "Migrate $file ..." --allowedTools "Edit"; done
Output: OK OK FAIL OK OK ...WhyThe FAIL lines are the point of asking for one word. You can find them, look at those files by hand, and leave the rest alone.
Two flags worth knowing
--max-turns caps how many agentic turns a run may take before it stops with
an error. --max-budget-usd caps how much a run may spend. Both work only in
print mode, which tells you something about who they are for: a run you are
watching can be stopped with Escape, and a run you are not watching cannot.
There is also --bare, which starts Claude Code without discovering hooks,
skills, plugins, MCP servers, auto memory or CLAUDE.md. That sounds like a
loss until you think about CI, where the point is that every machine produces
the same result. A hook sitting in a teammate's home directory should not be
able to change what your pipeline does, and under --bare it cannot, because
nothing reads it. Bare mode does not use your subscription login, so it expects
an API key in the environment.
What to take away
claude -p runs a prompt without an interactive session and exits with a
status code, which is what makes Claude Code usable inside scripts, hooks and
pipelines. The output formats decide what a script can do with the result:
plain text to read, json for a single object you can pull a field out of, and
stream-json for a running commentary. It reads standard input, so it composes
with the rest of your shell, and piping data in is often better than granting
the permission to go and fetch it. The fan-out pattern — a short-lived
invocation per file — is how large mechanical migrations get done, and the
habit that makes it work is refining the prompt on two or three files before
running the whole set. Above all, remember that unattended means the permission
prompt is not there to save you; --allowedTools is the boundary, so make it
narrow.
Next: running several Claudes at once without them editing the same file out from under each other.
Check yourself
5 questions · pass 4/5 to unlock Parallel Sessions and Worktrees
1.What does
claude -p "prompt"do?2.Why does
--allowedToolsmatter more in a-prun than in an interactive one?3.Which output format should a script use when it wants a single parseable object with the result and session metadata?
4.You need to run the same migration prompt over 2,000 files. What does the documentation recommend doing first?
5.What happens to a
-prun in auto mode when the classifier repeatedly blocks what Claude tries to do?
5 left to answer