AniUI Academy

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.

9 min read

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.txt

That 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.

  1. 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.

  2. Step 2

    Write the loop

    A shell loop that calls claude -p once per file, with the file path interpolated into the prompt.

  3. 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.

  4. Step 4

    Run the set

    Only now let it loose on the full list, with tools scoped to what the task actually needs.

The third step is the one people skip, and it is the one that pays.
for file in $(cat files.txt); do
  claude -p "Migrate $file from React to Vue. Return OK or FAIL." \
    --allowedTools "Edit" "Bash(git commit *)"
done

Each 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

terminal
  1. 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"

  2. Output: OK
  3. WhyOne 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.

  4. You typed: git diff src/components/Badge.tsx

  5. Output: -  <Button type="primary" size="sm">
    +  <Button variant="primary" size="small">
    -  import Button from '../ui/Button'
    +  import { Button } from '../ui/Button'
  6. 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.

  7. 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"

  8. Output: OK
  9. WhyThe 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.

  10. You typed: for file in $(cat files.txt); do claude -p "Migrate $file ..." --allowedTools "Edit"; done

  11. Output: OK
    OK
    FAIL
    OK
    OK
    ...
  12. 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.

Three files of refinement bought a batch of two thousand that mostly worked.

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

up to 50
  1. 1.What does claude -p "prompt" do?

  2. 2.Why does --allowedTools matter more in a -p run than in an interactive one?

  3. 3.Which output format should a script use when it wants a single parseable object with the result and session metadata?

  4. 4.You need to run the same migration prompt over 2,000 files. What does the documentation recommend doing first?

  5. 5.What happens to a -p run in auto mode when the classifier repeatedly blocks what Claude tries to do?

5 left to answer