claude -p runs a non-interactive session. Without --bare, it loads the same context an interactive session would: hooks from ~/.claude, MCP servers from .mcp.json, CLAUDE.md files, skills, plugins, auto-memory. That means your CI behavior depends on whatever happens to be configured on the runner, which varies by image and by developer machine.
--bare changes this. It skips all auto-discovery. Only flags you pass explicitly take effect.
claude --bare -p "Summarize this file" --allowedTools "Read"
--bare will become the default for -p in a future release.
Step 1: Basic CI invocation
claude --bare -p "Run the test suite and fix any failures" \
--allowedTools "Bash,Read,Edit" \
--permission-mode dontAsk
--allowedTools pre-approves specific tools so the run completes without prompting. --permission-mode dontAsk denies anything not in your permissions.allow rules or the built-in read-only command set, which is the right mode for locked-down CI.
acceptEdits is the looser alternative: it lets Claude write files without prompting and also auto-approves common filesystem commands (mkdir, touch, mv, cp). Other shell commands still need an explicit --allowedTools entry.
Step 2: Load context you actually need
In bare mode, nothing loads automatically. Pass context with flags:
| To load | Flag |
|---|---|
| System prompt text | --append-system-prompt "You are a security reviewer." |
| System prompt file | --append-system-prompt-file ./reviewer-prompt.txt |
| Settings file | --settings .claude/ci-settings.json |
| MCP servers | --mcp-config .claude/ci-mcp.json |
| Custom agents | --agents '{"my-agent": {"..."}}' |
| Plugin | --plugin-dir ./claude-plugin |
Authentication in bare mode skips OAuth and keychain reads. Provide credentials via ANTHROPIC_API_KEY, or pass a settings file with apiKeyHelper to --settings.
Step 3: Get structured output
Add --output-format json to get the response with session ID, cost, and metadata:
claude --bare -p "Summarize this project" --output-format json | jq -r '.result'
For real-time streaming (useful for long CI tasks where you want to see progress):
claude --bare -p "Refactor auth.py" \
--output-format stream-json \
--verbose \
--include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'
For structured output conforming to a schema:
claude --bare -p "Extract all TODO comments" \
--output-format json \
--json-schema '{"type":"object","properties":{"todos":{"type":"array","items":{"type":"string"}}},"required":["todos"]}' | \
jq '.structured_output.todos[]'
Step 4: Handle retries in CI
When Claude Code retries a failed API call, it emits a system/api_retry event in stream-json output:
{
"type": "system",
"subtype": "api_retry",
"attempt": 2,
"max_retries": 3,
"retry_delay_ms": 1000,
"error_status": 529,
"error": "server_error"
}
Use this to surface retry progress in your CI logs or implement a hard failure after N retries.
The system/init event at the start of the stream reports which plugins loaded and which failed:
{
"plugins": [{"name": "my-plugin", "path": "/path"}],
"plugin_errors": [{"plugin": "broken-plugin", "type": "load_error", "message": "..."}]
}
Fail the CI run when plugin_errors is non-empty if your workflow requires a specific plugin.
Footguns
Bare mode skips your project’s CLAUDE.md, including coding conventions. If a CLAUDE.md tells Claude to write TypeScript with strict null checks and no default exports, bare mode Claude does not see it. For CI tasks that need project conventions, pass them explicitly with --append-system-prompt-file. Forgetting this is the most common cause of bare-mode CI producing different results than interactive sessions.
User-invoked skills like /commit are not available in -p mode. Interactive commands that require the REPL are only available in interactive sessions. Describe the task instead: "Look at staged changes and create an appropriate commit" with --allowedTools "Bash(git diff *),Bash(git commit *)".
The 10MB stdin cap applies as of v2.1.128. If you pipe a file or log larger than 10MB, Claude Code exits with a clear error. Workaround: write the content to a file and reference the path in the prompt instead of piping.
Scoped --allowedTools rules use the permission rule syntax. Bash(npm test) allows only npm test, not arbitrary npm commands. Bash(npm *) allows any npm command. The trailing * with a space enables prefix matching. Without the space, Bash(git diff*) matches git diff-index, which is probably not what you want.
--bare does not load .env files. Credentials in .env are not sourced. Pass them via environment variables in your CI system, or use apiKeyHelper in a settings file passed to --settings.
Pipe patterns
# Review a PR diff for security vulnerabilities
gh pr diff "$1" | claude --bare -p \
--append-system-prompt "You are a security engineer. Review for OWASP Top 10." \
--output-format json | jq -r '.result'
# Explain a build error
cat build-error.txt | claude --bare -p "Explain the root cause in one paragraph" > explanation.txt
# Typo linter as a package.json script
# "lint:typo": "git diff main | claude --bare -p \"Report filename:line for each typo. Nothing else.\""
When NOT to use --bare
- Your task needs project-specific conventions from CLAUDE.md. Pass them with
--append-system-prompt-fileor avoid bare mode. - You need an MCP server that is configured in
.mcp.json. Pass it explicitly with--mcp-configor use interactive mode. - You are not in CI. Interactive mode is more appropriate for one-off development tasks;
--bareis for reproducible, non-interactive runs.