CertSafari
    CLAUDE-CERTIFIED-ARCHITECT-FOUNDATIONS-CCAR-F · Lessons

    Domain 3 · Lesson 18/30

    Headless Claude Code in CI: -p, Permissions and JSON Output

    Integrate Claude Code into CI/CD pipelines

    8 min read
    3.33% of exam
    6 sources
    Published 28 Sep 2026
    Docs as of 26 Sep 2026

    What you will be able to do

    • Run Claude Code non-interactively with -p so a pipeline job starts, finishes and exits on its own
    • Choose permission flags that stop a CI run from waiting on an approval nobody will give
    • Use --output-format json with --json-schema to get findings a script can parse and post as PR comments

    Key concept

    Headless (non-interactive) run — With the -p flag (long form --print), Claude Code takes one prompt, does the work, writes the result to stdout and exits. It never opens an interactive session. Every CI integration in this subdomain is built on that one-shot, scriptable run.

    1.From a chat session to a pipeline step

    Run plain claude and you get an interactive session: it waits for you to type, shows its work and asks before risky actions. A CI runner has no keyboard and nobody watching, so it needs a different contract. The job should hand over a prompt, get an answer back and move on to the next step. The -p flag (long form --print) gives you that contract. The CLI reference describes claude -p "query" as a way to query and then exit, and it lists piped input (cat file | claude -p "query") as a supported way to work.

    Because the run reads stdin and writes stdout, it fits into ordinary shell plumbing. A build log can go in and a written explanation can come out, with no human involved at any point.

    A headless run in a pipeline: the build log is piped in and the explanation is written to a filebash
    cat build-error.txt | claude -p 'concisely explain the root cause of this build error' > output.txt

    This is the same mechanism hosted integrations use. The GitLab setup adds a job to .gitlab-ci.yml that installs Claude Code and runs claude -p with the request as the prompt. When the GitHub Action receives a prompt input, it switches to automation mode and runs without waiting for anyone to mention @claude. On every platform, the building block is a single -p invocation.

    Sources123

    2.Why -p alone does not prevent a hang

    -p removes the chat, but it does not remove permission checks. A -p run starts in the default permission mode, and in that mode only reads go ahead without asking. An edit or a shell command still needs an approval. The headless docs include this as a real process state, a session left waiting for an answer to a permission prompt, and they note that sending SIGTERM at that point leaves the prompt unanswered. Nobody on a CI runner will ever answer it.

    To stop the hang, decide ahead of time what the run is allowed to do, using flags on the command line:

    - --allowedTools pre-approves specific tools or command patterns, such as "Read" or "Bash(npm test)". Treat the text inside the parentheses as a pattern, not a label. The approval covers every command the pattern matches, so make it no wider than the job needs. - --permission-mode dontAsk handles everything else: any action that would have prompted is denied instead. The run fails fast rather than stalling. - --permission-mode acceptEdits is the looser choice when the job's purpose is to change files.

    The permission-modes guide's recipe for CI: an exact allowlist, with every other action deniedbash
    claude -p "run the test suite" --permission-mode dontAsk --allowedTools "Bash(npm test)" "Read"
    Permission modes compared for unattended pipeline runs
    ModeWhat runs without askingFit for CI
    defaultReads onlyThis is where a -p run starts. Any non-read action needs approval nobody can give
    acceptEditsReads, file edits, and common filesystem commandsJobs that are meant to change code, as in the GitLab example
    dontAskReads and pre-approved tools; anything that would prompt is deniedLocked-down CI and scripts
    bypassPermissionsEverythingIsolated containers and VMs only

    A platform team wraps `claude` inside a GitHub Actions job that fires on every pull request to summarize the diff. The first run hangs until the job times out, with no output ever written to the log. The team confirms the API key secret is valid and the prompt text is correct. What is the most likely cause of the hang?

    Sources45

    3.Machine-parseable results: --output-format and --json-schema

    A run that no longer hangs still needs output a program can read. By default a headless run prints plain text, which works for a log but not for a script that has to post comments. --output-format offers three shapes:

    The three --output-format values for a -p run
    ValueWhat you get
    textPlain text output (the default)
    jsonStructured JSON with result, session ID, and metadata
    stream-jsonNewline-delimited JSON for real-time streaming

    --output-format json alone wraps the answer in an envelope, but the answer itself is still free text in the result field. To make the content follow a fixed shape, add --json-schema with a JSON Schema. The data that matches the schema appears in a separate structured_output field. The headless docs read the two fields differently: jq -r '.result' extracts the text, and jq '.structured_output' extracts the schema-shaped data.

    A schema-constrained run: the typed data is read from .structured_output, not from .resultbash
    claude -p "Extract function names from auth.py" \
      --output-format json \
      --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
      | jq '.structured_output'

    Sources4

    4.From findings to inline PR comments

    Put the pieces together and you have a review bot. The headless docs show the input side: pipe the pull request diff into claude -p, give the run a reviewer role with --append-system-prompt, and ask for JSON. Add a --json-schema that describes findings, and a later step can loop over structured_output and post each finding as an inline comment on the right file and line.

    A PR diff piped into a headless review that returns JSONbash
    gh pr diff "$1" | claude -p \
      --append-system-prompt "You are a security engineer. Review for vulnerabilities." \
      --output-format json

    There is also a built-in route. The /code-review skill accepts --comment, which posts findings on a GitHub pull request as inline comments. Skills can be invoked inside a -p prompt, and in non-interactive mode Claude Code waits for the review to finish and includes the findings in the response. On GitHub specifically, the Claude Code GitHub Action runs in automation mode whenever the workflow supplies a prompt input. Results go to the workflow run log unless the prompt tells Claude to post and gives it a tool that can.

    An engineering lead wants CI-invoked Claude Code reviews to consistently flag missing test coverage using the team's specific definition of a "valuable test" (asserts behavior, not implementation details) and to know which fixtures already exist in the test helpers directory. Where should this project-specific guidance be encoded so every CI run picks it up automatically without being repeated in each workflow prompt?

    Sources6

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.Adding -p is enough to make Claude Code safe to run unattended, because non-interactive mode never waits for input.Why is that wrong?

      A -p run starts in default mode, where only reads run without asking. Edits and commands still need approval, and the process can sit waiting on a permission prompt. Pre-approve tools with --allowedTools, or use dontAsk so that prompts become denials.

      Covered in Why -p alone does not prevent a hang

    2. 2.--output-format json on its own makes Claude return findings in whatever JSON shape the pipeline needs.Why is that wrong?

      json only adds an envelope: result, session ID and metadata. The answer in result is still text. A fixed shape needs --json-schema, and that data is read from structured_output.

      Covered in Machine-parseable results: --output-format and --json-schema

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 2.
      “runs once and prints the result. Good for CI hooks, pre-commit checks, or piping into other tools.”
      ↩︎ From a chat session to a pipeline step
      “runs once and prints the result. Good for CI hooks, pre-commit checks, or piping into other tools.”
      ↩︎ Key concept
    2. 3.
      “Automation mode: when the workflow provides a prompt input, Claude runs without waiting for a mention”
      ↩︎ From a chat session to a pipeline step
    3. 4.
      “Waiting for an answer to a permission prompt: if you send SIGTERM to the process, Claude Code leaves the prompt unanswered.”
      ↩︎ Why -p alone does not prevent a hang
      “json: structured JSON with result, session ID, and metadata”
      ↩︎ Machine-parseable results: --output-format and --json-schema
      “json: structured JSON with result, session ID, and metadata”
      ↩︎ Exam trap 2
    4. 5.
      “Reads and pre-approved tools; anything that would prompt is denied”
      ↩︎ Why -p alone does not prevent a hang
      “Reads and pre-approved tools; anything that would prompt is denied”
      ↩︎ Exam trap 1
    5. 6.
      “--comment: posts the findings on a GitHub pull request as inline comments, or on a GitLab merge request as a single note”
      ↩︎ From findings to inline PR comments
      “Claude Code waits for the review and includes the findings in the response”
      ↩︎ From findings to inline PR comments

    Continue to page 2 of 2

    Context for CI Claude Code Runs: CLAUDE.md, Existing Tests and Re-Reviews