Everything so far has assumed you, a terminal, and a conversation. But the review discipline you built in this part only scales if it runs when you are not there: on every PR, on a schedule, inside a pipeline. Claude Code was built for this from the start. The same binary that runs your interactive sessions is a command-line tool that reads stdin, writes stdout, and exits with a status code, which means everything you know about shell pipelines applies to an agent.
The concept: one flag, no conversation
Add -p (or --print) to any claude command and it runs non-interactively: execute the prompt, print the result, exit. No session UI, no back-and-forth.
claude -p "What does the auth module do?"Every CLI option works with -p, and three families matter for pipelines:
Output formats. --output-format takes text (default), json, or stream-json. The json format wraps the run in an envelope with fields your script can rely on: result (the text), is_error, num_turns, session_id, total_cost_usd, and a per-model cost breakdown. That cost field means every scripted invocation is self-accounting; pipe it into your logs and you have per-job spend tracking for free. stream-json emits one JSON object per line as events happen and requires --verbose; add --include-partial-messages to get token-level deltas for live display.
Stdin. Non-interactive mode reads piped input like any Unix tool:
cat build-error.txt | claude -p "Explain the root cause of this build failure" > explanation.mdContinuation. Runs are stateless by default: each -p starts fresh. When a pipeline genuinely needs multi-step state, capture the session and resume it:
session_id=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$session_id"Bare mode: reproducibility for CI
Here is the trap in putting claude -p in CI naively: it loads the same context an interactive session would. CLAUDE.md, hooks, plugins, MCP servers, auto memory, whatever exists in the working directory and ~/.claude. On your machine that is your rig; on a runner it is whatever the checkout and image happen to contain, and your teammate's hook or a committed .mcp.json changes the behavior of "the same" command.
--bare turns all of that off. Per the docs, it skips auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md; only flags you pass explicitly take effect. Auth must come from ANTHROPIC_API_KEY (bare mode skips OAuth and keychain reads), and you feed back exactly the context you want: --append-system-prompt-file for standing instructions, --settings for config, --mcp-config for servers, --allowedTools for permissions.
The position this course takes: interactive sessions inherit your rig; CI invocations declare theirs. Use --bare in anything automated, and pass context explicitly. Reproducibility is a verification property, and Part 3 will lean on it hard.
In practice: a diff reviewer that emits JSON
Piped input, one prompt, a declared output contract. This is the shape of most useful headless tools:
#!/usr/bin/env bash
# Review the diff against main; emit machine-readable verdict.
set -euo pipefail
git diff main...HEAD | claude --bare -p "You are reviewing a diff.
Respond with ONLY a JSON object, no prose, no code fences:
{
\"verdict\": \"approve\" | \"request_changes\",
\"issues\": [{ \"file\": string, \"line\": number | null,
\"severity\": \"blocker\" | \"nit\", \"note\": string }]
}
Flag only correctness and security problems, not style." \
--output-format json > review-envelope.json
# Envelope sanity, then extract the model's JSON from .result
jq -e '.is_error == false' review-envelope.json > /dev/null
jq -r '.result' review-envelope.json | jq . > review.json
echo "cost: $(jq -r '.total_cost_usd' review-envelope.json) USD"
jq -e '.verdict == "approve"' review.jsonTwo layers of JSON, and the distinction teaches the CLI's honest limit. The outer envelope (is_error, total_cost_usd) is guaranteed by --output-format json. The inner contract, your verdict and issues shape, is enforced only by the prompt; the CLI does not validate the model's text against a schema. Prompted contracts hold most of the time, and "most of the time" is exactly what jq -e is there to catch: if the model wrapped its JSON in prose, the parse fails, the script exits nonzero, and your pipeline treats it as the failure it is. When you need the contract guaranteed rather than prompted, that is schema-validated structured output, and it lives in the Agent SDK: Lesson 2.11.
Note also what the reviewer cannot do: with --bare and no --allowedTools, it has no pre-approved tools and nothing gets granted mid-run, because there is no human to answer a permission prompt. For a reviewer fed its input on stdin, that is not a limitation, it is the design: a read-nothing, write-nothing critic.
Into GitHub Actions
For GitHub automation you could script raw claude -p on a runner, but the maintained path is the official action, anthropics/claude-code-action@v1. Setup is one command in an interactive session: /install-github-app installs the GitHub App on your repo and walks you through adding the workflow and the ANTHROPIC_API_KEY secret. (You need repo admin rights; Bedrock and Google Cloud users follow the manual path in the docs.)
The default workflow answers @claude mentions in issues and PRs. For an unconditional review job on every PR, the workflow is small:
name: Claude PR Review
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Review this PR's diff for correctness and security issues only.
Post findings as a PR comment with file and line references.
If nothing is wrong, say so in one line.
claude_args: |
--max-turns 10The prompt input is your dispatch; claude_args passes CLI flags through, so everything from this lesson (--model, --allowedTools, --append-system-prompt) works in CI unchanged. The API key comes from GitHub Secrets, never hardcoded. Scope the workflow's permissions block to what the job does: a reviewer needs contents: read and pull-requests: write, not blanket write access.
Where it breaks
No human in the loop means no permission prompts. Interactively, a risky tool call stops and asks you. Headless, there is nobody to ask: the call either matches a pre-approval (--allowedTools, settings) or fails. People discover this as mysterious CI hangs or refusals, then reach for the biggest hammer, --dangerously-skip-permissions, on a runner that has repo write access and secrets in scope. Do not. Declare the minimal tool list the job needs, or give it none and feed everything on stdin like the reviewer above.
Prompted contracts drift. The inner-JSON pattern fails occasionally: a model that wants to be helpful adds "Here is the review:" above the JSON. Your script must treat unparseable output as a failed run, not silently pass. jq -e and set -euo pipefail are load-bearing in the example, not decoration.
Cost has no natural brake. An interactive session has you watching it. A workflow on pull_request: synchronize runs on every push to every open PR, and a retry loop around a flaky job multiplies that. Cap work with --max-turns in claude_args, watch total_cost_usd from the JSON envelope, and give scheduled jobs a budget review after the first week. Boring advice that has saved real money.
Lablab-2-9Build the JSON diff reviewer
Goal: Ship a headless reviewer script whose output a pipeline can parse, then wire the PR review workflow into a GitHub repo.
Prereqs: the agentic-practice repo pushed to GitHub, jq installed, an Anthropic API key (the Action needs one even if your local Claude Code uses a subscription).
- Create a branch in the practice repo and make a small, deliberately imperfect change: for example, a function that mutates its input array while claiming to be pure.
- Create
review-diff.shwith the script from this lesson. Make it executable and run it on your branch. - Inspect all three outputs:
review-envelope.json(findtotal_cost_usdandnum_turns),review.json(did the model catch your planted issue?), and the script's exit code (echo $?). - Break the contract on purpose: edit the prompt to remove the "ONLY a JSON object" constraint and add "Explain your reasoning first." Re-run and watch where the script fails. Restore the prompt.
- Wire the Action: run
/install-github-appin an interactive session in the repo, add theANTHROPIC_API_KEYsecret when prompted, then commit theclaude-review.ymlworkflow from this lesson. - Open a PR from your branch and watch the Actions tab: the job should run and post a review comment on the PR.
Verify
./review-diff.shexits 0 on a clean diff and nonzero when the review parses torequest_changes(or fails to parse), and you can state which happened on your branch.review.jsonis valid JSON matching the contract, and the planted bug appears inissues.- Your sabotage run in step 4 failed at the
jqparse, not silently. - The PR shows a comment from the Claude workflow run, and the run is green in the Actions tab.
>Troubleshooting
- Local script fails immediately with an auth error:
--bareignores your OAuth login; exportANTHROPIC_API_KEYin the shell running the script. jq: error ... cannot be parsed as JSON: the model broke the inner contract. Printjq -r '.result'to see what came back; tighten the prompt ("no code fences" catches the most common wrapper) and re-run.- Action fails with a permissions error: check the workflow's
permissionsblock and confirm the Claude GitHub App is installed on the repo (/install-github-appreports status when re-run). - Action never triggers: the workflow file must be on the repository's default branch before PR events fire it.
Knowledge check
Knowledge check
Sources
- Run Claude Code programmatically (-p, output formats, bare mode, resume, cost fields): https://code.claude.com/docs/en/headless (fetched July 2026)
- Claude Code GitHub Actions (quick setup, action inputs, claude_args, security guidance): https://code.claude.com/docs/en/github-actions (fetched July 2026)
- CLI reference (flag syntax cross-checked): https://code.claude.com/docs/en/cli-reference (fetched July 2026)
- anthropics/claude-code-action repository (maintenance check): https://github.com/anthropics/claude-code-action (fetched July 2026)