# Interactive vs Non-Interactive Claude Code: When to Pair, When to Script

> Source: <https://outofcontext.dev/blog/interactive-vs-non-interactive-claude-code/>
> Published: 2026-07-07 00:00:00+00:00

# Interactive vs Non-Interactive Claude Code: When to Pair, When to Script

# Interactive vs Non-Interactive Claude Code: When to Pair, When to Script

You already run `claude`

, watch it read the repo, approve a tool call, nudge it, and move on. That’s one mode. There’s a second one you may not have wired up yet, and it’s the one that turns Claude Code from a thing you talk to into a thing you build with.

Same binary. Same tools. Same agent loop. The only thing that changes is whether you’re steering it live or handing it a job and walking away:

`claude "prompt"`

— a live REPL. A pairing session.`claude -p "prompt"`

— one shot, prints to stdout, exits. A Unix command you can pipe, script, and put in CI.

Pick the wrong one and you either babysit a task that should have been a one-liner, or you fire off an unattended run that aborts the first time it needs a decision you didn’t pre-make. Here’s the split I actually use.

## Two ways to run the same agent

The mental model is one sentence: **the REPL is a conversation; -p is a command that returns a value.**

Everything downstream follows from that. In the REPL you’re present — you approve tools mid-run, ask follow-ups, change your mind. With `-p`

you’re not there, so every decision has to be made up front, and the output is something a script consumes. Nothing about the agent’s capability changes between them. What changes is who’s in the loop.

## Interactive: `claude`

and `claude "prompt"`

```
claude                          # start a session
claude "explain the auth module and suggest a refactor"   # seed it with a first prompt
```

Both open the REPL. `claude "prompt"`

just skips the first thing you’d type. From there you get the interactive affordances:

**Mid-run approvals**— Claude asks before it runs a tool; you say yes or no.** Permission cycling**—`Shift+Tab`

moves through permission modes without restarting.**Follow-ups**— the conversation holds context, so you can steer turn by turn.

Reach for this when the task is exploratory, ambiguous, or needs human judgment somewhere in the middle — the cases where you can’t write the whole spec before you start. It’s a one-off you think through with Claude, not a step in a pipeline.

## Non-interactive: `claude -p`

/ `--print`

```
claude -p "What does the auth module do?"
```

`-p`

(or `--print`

) runs the prompt once, prints the response, and exits. No REPL, no interactivity. Because it reads stdin and writes stdout, it behaves like any other command-line tool — which means you can pipe into it and redirect out of it:

```
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt
```

For scripts and CI, add `--bare`

:

```
claude --bare -p "Summarize this file" --allowedTools "Read"
```

`--bare`

skips auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and `CLAUDE.md`

. Without it, `claude -p`

loads the same context an interactive session would — including whatever happens to be configured in the working directory or `~/.claude`

. Bare mode is how you get the *same result on every machine*: a teammate’s hook or a project `.mcp.json`

server won’t silently change the run, because bare mode never reads them. Only the flags you pass explicitly take effect.

Two things to know: in bare mode Claude has Bash, file-read, and file-edit tools, and you pass anything else in by flag (`--append-system-prompt`

, `--mcp-config`

, `--settings`

, …). And it skips OAuth and keychain, so auth has to come from `ANTHROPIC_API_KEY`

(or an `apiKeyHelper`

in `--settings`

).

The docs call

`--bare`

the recommended mode for scripted and SDK calls, and say itwill become the default forWiring it in now is future-proofing, not extra work.`-p`

in a future release.

`--bare`

is the reproducibility twin of the interactive-side debugging flag: when a live session misbehaves, [ --safe-mode](/blog/claude-code-safe-mode/) strips the same customization layers so you can find which one broke. One skips your context for consistency; the other skips it to isolate a fault.

## Making `-p`

machine-readable

Text output is fine for a human reading a terminal. For a script, use `--output-format`

:

`text`

(default) — plain text.`json`

— structured JSON: the answer lands in`.result`

, alongside`session_id`

,`total_cost_usd`

, and a per-model cost breakdown.`stream-json`

— newline-delimited JSON events, token by token.

Parse `json`

with `jq`

:

```
claude -p "Summarize this project" --output-format json | jq -r '.result'
```

Need the output to conform to a shape? Pair `--output-format json`

with `--json-schema`

. The result lands in `.structured_output`

— the extract-and-pipe use case:

```
claude -p "Extract the main function names from auth.py" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
  | jq '.structured_output'
```

For live progress, `stream-json`

with `--verbose --include-partial-messages`

emits an event per token. It also emits `system/init`

(session metadata: model, tools, MCP servers, loaded plugins) and `system/api_retry`

(attempt count, delay, error category) — useful for CI observability and failing a build when a plugin didn’t load.

## Sessions without the REPL

Headless runs can still be multi-step. Two flags carry state across invocations:

`--continue`

/`-c`

— reload the most recent conversation in this directory.`--resume`

/`-r <id|name>`

— resume a specific session.

```
claude -p "Review this codebase for performance issues"
claude -p "Now focus on the database queries" --continue
```

For multiple parallel conversations, capture the `session_id`

and resume it by hand:

```
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"
```

Run both from the same directory — session lookup is scoped to the current project directory and its git worktrees.

## Guardrails: bounding a headless run

An unattended `-p`

run can loop on its own, so bound it. These two are **print-mode only**:

`--max-turns N`

— cap agentic turns; error on overflow.`--max-budget-usd N`

— hard dollar ceiling per invocation, stop when hit.

Then set the permission posture. `--permission-mode`

takes one of: `default`

, `acceptEdits`

, `plan`

, `auto`

, `dontAsk`

, `bypassPermissions`

, `manual`

. The two that matter for scripting:

`dontAsk`

— denies anything not in your`permissions.allow`

rules or the built-in read-only command set. This is your locked-down CI mode.`acceptEdits`

— auto-approves file writes plus common filesystem commands (`mkdir`

,`touch`

,`mv`

,`cp`

). Other shell commands and network calls still need an allow rule, or the run aborts when one is attempted.

For finer control, allow specific tools with permission-rule syntax:

```
claude -p "Apply the lint fixes" --allowedTools "Bash(git diff *),Edit"
```

Mind the space before `*`

: `Bash(git diff *)`

prefix-matches `git diff …`

, while `Bash(git diff*)`

would also match `git diff-index`

.

There’s also `--dangerously-skip-permissions`

(equivalent to `--permission-mode bypassPermissions`

). It approves everything. Justified in a throwaway sandbox or container you’ve isolated; reckless against a repo you care about or anything with network and secrets. Reach for `dontAsk`

plus an explicit allow-list first. (The same review-vs-autonomy trade-off shows up in the editor, covered in [Cursor auto-review vs YOLO](/blog/cursor-auto-review-vs-yolo/).)

## Real recipes

These are the payoff. Every one is a real `-p`

invocation.

**Explain a failure.** Pipe a log in, get the root cause out:

```
cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt
```

**Project linter as a package.json script.** Piping the diff means Claude doesn’t need Bash permission to read it; the escaped quotes keep it portable to Windows:

```
{
  "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.\""
  }
}
```

**Commit helper.** Review staged changes and write the message:

```
claude -p "Look at my staged changes and create an appropriate commit" \
  --allowedTools "Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)"
```

**PR security review.** Feed a PR diff in, reshape Claude’s role with `--append-system-prompt`

, get JSON out:

```
gh pr diff "$1" | claude -p \
  --append-system-prompt "You are a security engineer. Review for vulnerabilities." \
  --output-format json
```

**Batch loop.** One `-p`

call per item, collect the JSON:

```
for f in src/**/*.py; do
  claude --bare -p "Summarize what $f does in one sentence" \
    --allowedTools "Read" --output-format json | jq -r '.result'
done
```

User-invoked skills and custom commands work in `-p`

too — put `/skill-name`

in the prompt string and Claude Code expands it before running. Commands that open an interactive dialog, like `/login`

, don’t work headless.

## Which mode, when

| Task shape | Mode | Key flags |
|---|---|---|
| Exploratory, ambiguous, needs mid-run judgment | Interactive | — |
| One-off you want to think through | Interactive | `claude "prompt"` |
| Deterministic, repeatable, composable | `-p` | `--bare` , `--allowedTools` |
| Unattended / CI | `-p` | `--bare` , `--permission-mode dontAsk` , `--max-budget-usd` |
| Output another program parses | `-p` | `--output-format json` , `--json-schema` |
| Multi-step scripted workflow | `-p` | `--output-format json` + `--resume "$session_id"` |

Rule of thumb: if a human needs to be in the loop, use the REPL. If a program is, use `-p`

.

## Gotchas

Things that bite the first time you script this:

**No mid-run approvals.** In`-p`

there’s nobody to ask. Decide permissions up front, or the run aborts the first time it hits an un-approved tool. This is the single most common headless surprise.**Quoting and escaping.** Shell quoting rules apply, and in`package.json`

you escape the double quotes (`\"…\"`

) for Windows portability.**Piped stdin is capped at 10MB**(as of v2.1.128). Exceed it and Claude Code exits with a clear error and non-zero status. For larger input, write it to a file and reference the path in the prompt instead of piping.**Cost.**`-p`

can loop autonomously. Pair`--max-turns`

and`--max-budget-usd`

, and track spend with`total_cost_usd`

from`--output-format json`

.Great for reproducibility, surprising if you expected your`--bare`

skips your local context.`CLAUDE.md`

or MCP servers to load. If a bare run behaves differently than your interactive session, this is usually why.**Background tasks at exit.** A background Bash task (dev server, watch build) is terminated ~5s after Claude returns its result and stdin closes (a fix landed in v2.1.163 so a never-exiting process no longer holds the run open forever). Background subagents and workflows are waited on instead, capped at ten minutes by default since v2.1.182.

## TL;DR

Use the REPL to *think with* Claude. Use `-p`

to *build with* it. The move: take something you keep doing by hand in the REPL, ship it as a one-liner, then graduate the one-liner to a `package.json`

script, a git hook, or a CI job. Add `--bare`

and a permission posture, and it runs the same everywhere.

## Sources

All flags and behavior verified against primary docs on 2026-07-06.

- Claude Code CLI reference —
[https://code.claude.com/docs/en/cli-reference](https://code.claude.com/docs/en/cli-reference)(flag names and behavior, permission-mode enum, session flags,`--max-budget-usd`

,`--bare`

). - Run Claude Code programmatically (headless) —
[https://code.claude.com/docs/en/headless](https://code.claude.com/docs/en/headless)(stdin piping and 10MB cap, output formats, JSON fields,`--bare`

default note, recipes, session capture, background-task grace period).
