Study guides / CCAR-F / Domain 3

Claude Code Configuration & Workflows · Lesson 6 of 6

3.6 - CI/CD Integration

Master running Claude Code in CI/CD pipelines: the -p flag for non-interactive mode, structured JSON output, session isolation for reviews, and incremental review context

Drop Claude Code into a CI/CD pipeline and it stops being an interactive developer tool and becomes an automated review and generation engine. The exam tests five concepts in this task statement, and the -p flag is the single most directly tested item (it's Question 10 in the sample question set).

The -p Flag: Non-Interactive Mode

Claude Code defaults to interactive mode: it expects keyboard input and shows a conversational interface. A CI pipeline has no keyboard. Without the -p flag, the job hangs forever, waiting for input that never comes.

# WRONG - hangs in CI
claude "Analyse this pull request for security issues"

# CORRECT - runs non-interactively
claude -p "Analyse this pull request for security issues"

The -p flag (also --print) switches Claude Code to print mode: it processes the prompt, outputs the result to stdout, and exits. No interactive input required.

This one is pure memorisation. The exam shows a CI job hanging, logs of Claude waiting for input, and asks you to pick the fix. The answer is the -p flag. Not CLAUDE_HEADLESS=true (doesn't exist). Not --batch (doesn't exist). Not stdin redirection from /dev/null (doesn't properly address Claude Code's interactive mode).

Key Concept

The -p flag is the single most directly testable fact in Domain 3. It is Question 10 in the official sample questions. When you see a CI pipeline hanging and logs showing Claude waiting for input, the answer is always -p.

Structured Output for CI

In CI, Claude Code's output has to be machine-parseable. No human is reading it. Automated systems process it to post inline PR comments, update dashboards, or trigger downstream workflows.

Two flags work together:

claude -p \
  --output-format json \
  --json-schema '{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"file":{"type":"string"},"line":{"type":"integer"},"severity":{"type":"string"},"message":{"type":"string"}}}}}}' \
  "Review this PR for security issues"

The schema-conforming data lands in the envelope's structured_output field - extract it with jq '.structured_output', not from the top level. That gives automated systems validated findings they can:

Session Context Isolation

The same Claude session that generated code is less effective at reviewing its own changes. This isn't a theoretical worry; it's a measurable effect.

Why self-review is weaker:

When Claude generates code in a session, it builds up reasoning context: why it chose this approach, what tradeoffs it considered, what alternatives it rejected. Ask it to review the same code in the same session and it keeps all of that. It's less likely to question decisions it already justified to itself.

The fix: independent review instances

Use a separate Claude Code invocation for review - one that has no access to the generation session's reasoning context. The independent reviewer evaluates the code on its own merits, without the bias of prior justification.

# Step 1: Generate code (session A)
claude -p "Implement the authentication middleware"

# Step 2: Review code (session B - independent, no shared context)
claude -p "Review the authentication middleware for security issues, error handling gaps, and edge cases"

This concept connects to Domain 4 (multi-instance review architectures) and Domain 5 (context management). The exam tests it in CI/CD scenarios specifically.

Incremental Review Context

Automated reviews run on every push. Without context about previous reviews, each run analyses the entire PR from scratch, so it re-derives the same findings every time. A genuinely fixed issue drops out on its own, because the changed code no longer triggers it. The ones that keep coming back are the issues the developer saw and deliberately chose not to change; a context-free re-scan cannot tell those apart from new problems, so it flags them again on every push.

The fix: include prior review findings in context and instruct Claude to report only new or still-unaddressed issues.

claude -p \
  --output-format json \
  "Review this PR. Here are the findings from the previous review:
  ${PREVIOUS_FINDINGS}

  Report ONLY:
  1. New issues not in the previous findings
  2. Issues from the previous findings that are still present

  Do NOT re-report previous findings the developer has already reviewed and chosen not to act on."

Duplicate comments erode developer trust. If every push generates the same five comments regardless of whether the developer fixed the issues, developers stop reading the comments. Incremental review context preserves the signal-to-noise ratio.

CLAUDE.md for CI Context

When Claude Code runs in CI, it reads the project's CLAUDE.md files exactly as it does interactively. So CLAUDE.md is how you feed project-specific context to a CI-invoked run:

Without this context in CLAUDE.md, CI-invoked test generation produces low-value boilerplate. With it, generated tests follow the team's patterns and add genuine coverage.

# .claude/CLAUDE.md - CI-relevant section
## Testing Standards

- Tests must use the factory pattern from test/factories/ for data creation
- Integration tests connect to the test database via test/setup/db.ts
- Do not test private implementation details - test public API contracts
- Coverage target: 80% branch coverage for new code
- Available fixtures: test/fixtures/users.json, test/fixtures/orders.json

CLI Flags Reference

The -p flag is the most directly tested flag, but the exam also expects familiarity with the flags that shape a headless run: how output is formatted, which system prompt is used, and how permissions and tools are scoped. These flags work with claude -p in CI and with the interactive claude command.

System prompt flags. Claude Code provides four flags here, and the exam tests the append-versus-replace distinction:

Flag Effect
--system-prompt "<text>" Replaces the entire default system prompt
--system-prompt-file <path> Replaces the default prompt with a file's contents
--append-system-prompt "<text>" Appends text to the default prompt
--append-system-prompt-file <path> Appends a file's contents to the default prompt

Append when Claude should stay a coding assistant that also follows your extra rules. Appending keeps the default tool guidance, safety instructions, and coding conventions, so you only supply what differs. Replace when the identity or permission model differs from Claude Code's, like a non-coding agent in a pipeline no human watches. Replacing drops the entire default prompt, so you own everything the task still needs.

Headless output and limits (print mode).

Flag Effect
--output-format text|json|stream-json Output shape for -p; json and stream-json are machine-parseable
--input-format text|stream-json Input shape for -p
--json-schema '<schema>' Schema-validated output for -p; with --output-format json it lands in the envelope's structured_output field
--max-turns <n> Cap the number of agentic turns, then exit
--verbose Full turn-by-turn output

Permissions, tools, and context.

Flag Effect
--permission-mode <mode> Start in default, acceptEdits, plan, auto, dontAsk, or bypassPermissions
--allowedTools "<rules>" Tools that run without a permission prompt, e.g. "Bash(git diff *)" "Read"
--disallowedTools "<rules>" Deny rules; a bare tool name removes the tool from context entirely
--tools "Bash,Edit,Read" Restrict which built-in tools are available at all
--add-dir <path> Add a directory Claude may read and edit (grants file access, not configuration discovery)
--model <alias|name> Set the session model (sonnet, opus, or a full model name)

Session and start-up. -c / --continue resumes the most recent conversation in the current directory, and -r / --resume <id|name> resumes a specific session. --bare is minimal mode: it skips auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md so scripted calls start faster, leaving Claude with the Bash and file read/edit tools only. Reach for --bare when you want a fast, predictable scripted run and don't need project configuration loaded.

Providing Existing Tests to Avoid Duplication

When running test generation in CI, include existing test files in context. Without them, Claude Code may suggest tests that already exist, wasting developer review time. Including existing tests enables Claude to identify coverage gaps rather than duplicating existing scenarios.

Batch API vs Real-Time for CI Workflows

The Message Batches API offers 50% cost savings but has processing times up to 24 hours with no guaranteed latency SLA. This creates a clear decision boundary:

Workflow type API choice Reason
Pre-merge checks (blocking) Real-time (synchronous) Developers wait for results
Overnight technical debt reports Batch API Not time-sensitive, 50% savings
Weekly code audit Batch API Scheduled, latency-tolerant
Nightly test generation Batch API Runs overnight, reviewed next morning

Pre-merge checks are blocking workflows. Developers can't merge until the check completes. The Batch API is unsuitable here because it gives no latency guarantee. The exam tests this distinction directly (Sample Question 11).

Exam traps

Practice question

A CI pipeline script runs claude with a prompt but the job hangs indefinitely. Logs show Claude Code is waiting for interactive input. What is the correct fix?

  • A Add the -p flag so Claude Code runs in non-interactive print mode Correct

    The -p (--print) flag runs Claude Code in non-interactive mode. It processes the prompt, outputs the result to stdout, and exits without waiting for user input. This is the documented approach for CI/CD integration.

  • B Set the environment variable CLAUDE_HEADLESS=true before running the command

    CLAUDE_HEADLESS is not a real Claude Code environment variable. This option references a feature that does not exist.

  • C Redirect stdin from /dev/null to prevent interactive prompts

    Unix stdin redirection is a generic workaround that does not properly address Claude Code interactive mode. The -p flag is the correct, documented approach.

  • D Add the --batch flag to enable batch processing mode

    --batch is not a real Claude Code CLI flag. This option references a feature that does not exist.

Build exercise: Set Up a CI/CD Pipeline with Claude Code

Advanced · 45 minutes

You'll practice:

  1. Write a CI script that runs Claude Code with the -p flag for non-interactive PR analysis

    The -p flag is the single most directly testable fact in Domain 3. Without it, the CI job hangs indefinitely waiting for interactive input. This is Question 10 in the official sample questions.

    You should see: A CI script (GitHub Actions YAML, GitLab CI, or similar) that invokes claude -p with a review prompt. The job completes successfully without hanging. The output is printed to stdout and captured by the CI system.

    Hints
    1. The -p flag (also --print) switches Claude Code from interactive mode to print mode. Without it, CI jobs hang.
    2. Add -p immediately after the claude command in your CI script. The prompt follows as a string argument.
    3. Example GitHub Actions step:
      - name: Review PR
        run: |
          claude -p "Analyse the changes in this pull request for security issues, error handling gaps, and test coverage."
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  2. Add --output-format json and --json-schema to produce structured findings with file, line, severity, and message fields

    CI output must be machine-parseable. Automated systems need structured JSON to post inline PR comments, filter by severity, and track findings across runs. Human-readable text output cannot be reliably parsed by downstream tools.

    You should see: The Claude Code output is a JSON envelope whose structured_output field conforms to the specified schema. Each finding has file, line, severity, and message fields. Piping the output to jq .structured_output extracts the validated data without errors.

    Hints
    1. Add --output-format json and --json-schema with a JSON schema string defining the expected output structure. The validated data lands in the envelope, not at the top level.
    2. The --json-schema flag takes a JSON schema as a string argument. Define properties for file, line, severity, and message. Read the result from the structured_output field of the JSON envelope.
    3. Update your CI command:
      claude -p \
        --output-format json \
        --json-schema '{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"file":{"type":"string"},"line":{"type":"integer"},"severity":{"type":"string"},"message":{"type":"string"}}}}}}' \
        "Review this PR for security issues"
  3. Configure the pipeline to parse the JSON output and post findings as inline PR comments

    Inline PR comments at exact file and line numbers provide actionable feedback. Generic PR-level comments are ignored. Structured JSON output makes precise inline commenting possible.

    You should see: Each finding from the JSON output appears as an inline comment on the PR at the exact file and line number. Severity levels are visible. Developers can see the finding in context alongside the code it references.

    Hints
    1. Parse the JSON output and use your CI platform API (GitHub, GitLab) to post review comments at specific file and line locations.
    2. Use jq to extract findings from the JSON output, then iterate over them to post PR review comments via the GitHub API or gh CLI.
    3. Example pipeline script:
      FINDINGS=$(claude -p --output-format json --json-schema '...' "Review this PR")
      echo "$FINDINGS" | jq -r '.structured_output.findings[] | "\(.file):\(.line) [\(.severity)] \(.message)"' | while read finding; do
        # Post as inline PR comment using gh api
        gh api repos/:owner/:repo/pulls/:pr/comments ...
      done
  4. Add a section to CLAUDE.md documenting testing standards, available fixtures, and review criteria for CI-invoked Claude Code

    Claude Code reads CLAUDE.md in CI just as in interactive mode. Without project context, CI-invoked test generation produces low-value boilerplate. With testing standards and fixture documentation, generated tests follow team patterns.

    You should see: The CLAUDE.md file contains a clearly marked CI-relevant section with testing standards, available fixture paths, and review severity criteria. CI-invoked Claude Code produces tests using the documented factories and fixtures rather than generic boilerplate.

    Hints
    1. Add a dedicated section to your .claude/CLAUDE.md with information that CI-invoked Claude Code needs for quality output.
    2. Include: test naming patterns, available factory functions, fixture file paths, coverage targets, and what constitutes critical vs minor review findings.
    3. Add to .claude/CLAUDE.md:
      ## Testing Standards (CI Context)
      
      - Use factory functions from test/factories/ for data creation
      - Integration tests use the test database via test/setup/db.ts
      - Do not test private implementation details
      - Coverage target: 80% branch coverage for new code
      - Available fixtures: test/fixtures/users.json, test/fixtures/orders.json
      
      ## Review Criteria
      - Critical: security issues, data loss risk, authentication bypass
      - Major: missing error handling, uncovered edge cases
      - Minor: naming conventions, style inconsistencies
  5. Set up two separate Claude Code invocations: one for code generation and an independent one for review (no shared session context)

    The same session that generated code is less effective at reviewing it because it retains reasoning context that biases it toward its own decisions. Independent review instances evaluate code on its own merits without prior justification bias.

    You should see: Two distinct claude -p invocations in the CI script: one for generation and one for review. They share no session context. The review invocation analyses the generated code independently. The review findings are more thorough than self-review in the same session.

    Hints
    1. Each claude -p invocation creates an independent session. Ensure the review invocation does not reference or continue the generation session.
    2. Run generation and review as separate steps in your CI pipeline. Each is an independent claude -p call with its own prompt.
    3. CI pipeline steps:
      # Step 1: Generate (Session A)
      - name: Generate tests
        run: claude -p "Generate unit tests for src/auth/middleware.ts"
      
      # Step 2: Review (Session B - independent)
      - name: Review generated tests
        run: claude -p "Review the test file at src/auth/middleware.test.ts for coverage gaps, edge cases, and assertion quality. Do not assume the tests are correct."
  6. Implement incremental review: store previous findings, include them in the next review run, and instruct Claude to report only new or still-unaddressed issues

    Without incremental context, each review run analyses the entire PR from scratch and produces duplicate comments. Duplicate comments erode developer trust - when the same five issues appear on every push regardless of fixes, developers stop reading them.

    You should see: The first review run produces findings and stores them (as a JSON artifact or file). Subsequent runs include the previous findings in context. The output contains only new issues or issues that remain unaddressed. Previously fixed issues do not reappear as comments.

    Hints
    1. Store review findings as a CI artifact after each run. On subsequent runs, retrieve the previous findings and include them in the prompt context.
    2. Save the JSON output as a CI artifact. On the next run, load it and include it in the prompt with instructions to report only new or still-present issues.
    3. Implementation pattern:
      # Load previous findings if they exist
      PREV=$(cat previous-findings.json 2>/dev/null || echo "[]")
      
      # Run review with incremental context
      claude -p --output-format json \
        "Review this PR. Previous findings: $PREV
        Report ONLY:
        1. New issues not in previous findings
        2. Issues from previous findings still present
        Do NOT re-report fixed issues."
      
      # Save current findings for next run
      cp output.json previous-findings.json

Sources