{"slug": "interactive-vs-non-interactive-claude-code-when-to-pair-when-to-script", "title": "Interactive vs Non-Interactive Claude Code: When to Pair, When to Script", "summary": "Anthropic's Claude Code tool can be run in two modes: interactive REPL for exploratory tasks requiring human judgment, and non-interactive -p mode for scripting and CI pipelines. The -p mode with --bare flag ensures reproducibility by skipping hooks and customizations, making it suitable for automated workflows.", "body_md": "# Interactive vs Non-Interactive Claude Code: When to Pair, When to Script\n\n# Interactive vs Non-Interactive Claude Code: When to Pair, When to Script\n\nYou already run `claude`\n\n, 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.\n\nSame 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:\n\n`claude \"prompt\"`\n\n— a live REPL. A pairing session.`claude -p \"prompt\"`\n\n— one shot, prints to stdout, exits. A Unix command you can pipe, script, and put in CI.\n\nPick 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.\n\n## Two ways to run the same agent\n\nThe mental model is one sentence: **the REPL is a conversation; -p is a command that returns a value.**\n\nEverything downstream follows from that. In the REPL you’re present — you approve tools mid-run, ask follow-ups, change your mind. With `-p`\n\nyou’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.\n\n## Interactive: `claude`\n\nand `claude \"prompt\"`\n\n```\nclaude                          # start a session\nclaude \"explain the auth module and suggest a refactor\"   # seed it with a first prompt\n```\n\nBoth open the REPL. `claude \"prompt\"`\n\njust skips the first thing you’d type. From there you get the interactive affordances:\n\n**Mid-run approvals**— Claude asks before it runs a tool; you say yes or no.** Permission cycling**—`Shift+Tab`\n\nmoves through permission modes without restarting.**Follow-ups**— the conversation holds context, so you can steer turn by turn.\n\nReach 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.\n\n## Non-interactive: `claude -p`\n\n/ `--print`\n\n```\nclaude -p \"What does the auth module do?\"\n```\n\n`-p`\n\n(or `--print`\n\n) 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:\n\n```\ncat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt\n```\n\nFor scripts and CI, add `--bare`\n\n:\n\n```\nclaude --bare -p \"Summarize this file\" --allowedTools \"Read\"\n```\n\n`--bare`\n\nskips auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and `CLAUDE.md`\n\n. Without it, `claude -p`\n\nloads the same context an interactive session would — including whatever happens to be configured in the working directory or `~/.claude`\n\n. Bare mode is how you get the *same result on every machine*: a teammate’s hook or a project `.mcp.json`\n\nserver won’t silently change the run, because bare mode never reads them. Only the flags you pass explicitly take effect.\n\nTwo 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`\n\n, `--mcp-config`\n\n, `--settings`\n\n, …). And it skips OAuth and keychain, so auth has to come from `ANTHROPIC_API_KEY`\n\n(or an `apiKeyHelper`\n\nin `--settings`\n\n).\n\nThe docs call\n\n`--bare`\n\nthe recommended mode for scripted and SDK calls, and say itwill become the default forWiring it in now is future-proofing, not extra work.`-p`\n\nin a future release.\n\n`--bare`\n\nis 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.\n\n## Making `-p`\n\nmachine-readable\n\nText output is fine for a human reading a terminal. For a script, use `--output-format`\n\n:\n\n`text`\n\n(default) — plain text.`json`\n\n— structured JSON: the answer lands in`.result`\n\n, alongside`session_id`\n\n,`total_cost_usd`\n\n, and a per-model cost breakdown.`stream-json`\n\n— newline-delimited JSON events, token by token.\n\nParse `json`\n\nwith `jq`\n\n:\n\n```\nclaude -p \"Summarize this project\" --output-format json | jq -r '.result'\n```\n\nNeed the output to conform to a shape? Pair `--output-format json`\n\nwith `--json-schema`\n\n. The result lands in `.structured_output`\n\n— the extract-and-pipe use case:\n\n```\nclaude -p \"Extract the main function names from auth.py\" \\\n  --output-format json \\\n  --json-schema '{\"type\":\"object\",\"properties\":{\"functions\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"functions\"]}' \\\n  | jq '.structured_output'\n```\n\nFor live progress, `stream-json`\n\nwith `--verbose --include-partial-messages`\n\nemits an event per token. It also emits `system/init`\n\n(session metadata: model, tools, MCP servers, loaded plugins) and `system/api_retry`\n\n(attempt count, delay, error category) — useful for CI observability and failing a build when a plugin didn’t load.\n\n## Sessions without the REPL\n\nHeadless runs can still be multi-step. Two flags carry state across invocations:\n\n`--continue`\n\n/`-c`\n\n— reload the most recent conversation in this directory.`--resume`\n\n/`-r <id|name>`\n\n— resume a specific session.\n\n```\nclaude -p \"Review this codebase for performance issues\"\nclaude -p \"Now focus on the database queries\" --continue\n```\n\nFor multiple parallel conversations, capture the `session_id`\n\nand resume it by hand:\n\n```\nsession_id=$(claude -p \"Start a review\" --output-format json | jq -r '.session_id')\nclaude -p \"Continue that review\" --resume \"$session_id\"\n```\n\nRun both from the same directory — session lookup is scoped to the current project directory and its git worktrees.\n\n## Guardrails: bounding a headless run\n\nAn unattended `-p`\n\nrun can loop on its own, so bound it. These two are **print-mode only**:\n\n`--max-turns N`\n\n— cap agentic turns; error on overflow.`--max-budget-usd N`\n\n— hard dollar ceiling per invocation, stop when hit.\n\nThen set the permission posture. `--permission-mode`\n\ntakes one of: `default`\n\n, `acceptEdits`\n\n, `plan`\n\n, `auto`\n\n, `dontAsk`\n\n, `bypassPermissions`\n\n, `manual`\n\n. The two that matter for scripting:\n\n`dontAsk`\n\n— denies anything not in your`permissions.allow`\n\nrules or the built-in read-only command set. This is your locked-down CI mode.`acceptEdits`\n\n— auto-approves file writes plus common filesystem commands (`mkdir`\n\n,`touch`\n\n,`mv`\n\n,`cp`\n\n). Other shell commands and network calls still need an allow rule, or the run aborts when one is attempted.\n\nFor finer control, allow specific tools with permission-rule syntax:\n\n```\nclaude -p \"Apply the lint fixes\" --allowedTools \"Bash(git diff *),Edit\"\n```\n\nMind the space before `*`\n\n: `Bash(git diff *)`\n\nprefix-matches `git diff …`\n\n, while `Bash(git diff*)`\n\nwould also match `git diff-index`\n\n.\n\nThere’s also `--dangerously-skip-permissions`\n\n(equivalent to `--permission-mode bypassPermissions`\n\n). 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`\n\nplus 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/).)\n\n## Real recipes\n\nThese are the payoff. Every one is a real `-p`\n\ninvocation.\n\n**Explain a failure.** Pipe a log in, get the root cause out:\n\n```\ncat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt\n```\n\n**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:\n\n```\n{\n  \"scripts\": {\n    \"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.\\\"\"\n  }\n}\n```\n\n**Commit helper.** Review staged changes and write the message:\n\n```\nclaude -p \"Look at my staged changes and create an appropriate commit\" \\\n  --allowedTools \"Bash(git diff *),Bash(git log *),Bash(git status *),Bash(git commit *)\"\n```\n\n**PR security review.** Feed a PR diff in, reshape Claude’s role with `--append-system-prompt`\n\n, get JSON out:\n\n```\ngh pr diff \"$1\" | claude -p \\\n  --append-system-prompt \"You are a security engineer. Review for vulnerabilities.\" \\\n  --output-format json\n```\n\n**Batch loop.** One `-p`\n\ncall per item, collect the JSON:\n\n```\nfor f in src/**/*.py; do\n  claude --bare -p \"Summarize what $f does in one sentence\" \\\n    --allowedTools \"Read\" --output-format json | jq -r '.result'\ndone\n```\n\nUser-invoked skills and custom commands work in `-p`\n\ntoo — put `/skill-name`\n\nin the prompt string and Claude Code expands it before running. Commands that open an interactive dialog, like `/login`\n\n, don’t work headless.\n\n## Which mode, when\n\n| Task shape | Mode | Key flags |\n|---|---|---|\n| Exploratory, ambiguous, needs mid-run judgment | Interactive | — |\n| One-off you want to think through | Interactive | `claude \"prompt\"` |\n| Deterministic, repeatable, composable | `-p` | `--bare` , `--allowedTools` |\n| Unattended / CI | `-p` | `--bare` , `--permission-mode dontAsk` , `--max-budget-usd` |\n| Output another program parses | `-p` | `--output-format json` , `--json-schema` |\n| Multi-step scripted workflow | `-p` | `--output-format json` + `--resume \"$session_id\"` |\n\nRule of thumb: if a human needs to be in the loop, use the REPL. If a program is, use `-p`\n\n.\n\n## Gotchas\n\nThings that bite the first time you script this:\n\n**No mid-run approvals.** In`-p`\n\nthere’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`\n\nyou escape the double quotes (`\\\"…\\\"`\n\n) 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`\n\ncan loop autonomously. Pair`--max-turns`\n\nand`--max-budget-usd`\n\n, and track spend with`total_cost_usd`\n\nfrom`--output-format json`\n\n.Great for reproducibility, surprising if you expected your`--bare`\n\nskips your local context.`CLAUDE.md`\n\nor 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.\n\n## TL;DR\n\nUse the REPL to *think with* Claude. Use `-p`\n\nto *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`\n\nscript, a git hook, or a CI job. Add `--bare`\n\nand a permission posture, and it runs the same everywhere.\n\n## Sources\n\nAll flags and behavior verified against primary docs on 2026-07-06.\n\n- Claude Code CLI reference —\n[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`\n\n,`--bare`\n\n). - Run Claude Code programmatically (headless) —\n[https://code.claude.com/docs/en/headless](https://code.claude.com/docs/en/headless)(stdin piping and 10MB cap, output formats, JSON fields,`--bare`\n\ndefault note, recipes, session capture, background-task grace period).", "url": "https://wpnews.pro/news/interactive-vs-non-interactive-claude-code-when-to-pair-when-to-script", "canonical_source": "https://outofcontext.dev/blog/interactive-vs-non-interactive-claude-code/", "published_at": "2026-07-07 00:00:00+00:00", "updated_at": "2026-07-07 05:05:45.843290+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "large-language-models", "generative-ai"], "entities": ["Anthropic", "Claude Code"], "alternates": {"html": "https://wpnews.pro/news/interactive-vs-non-interactive-claude-code-when-to-pair-when-to-script", "markdown": "https://wpnews.pro/news/interactive-vs-non-interactive-claude-code-when-to-pair-when-to-script.md", "text": "https://wpnews.pro/news/interactive-vs-non-interactive-claude-code-when-to-pair-when-to-script.txt", "jsonld": "https://wpnews.pro/news/interactive-vs-non-interactive-claude-code-when-to-pair-when-to-script.jsonld"}}