- Advanced
- Headless Ci
Headless mode: Claude Code without an interface
Headless mode lets you use Claude Code as a standard command-line tool: it receives an instruction, executes it, and returns a result, with no interactive interface, no confirmation prompts, no waiting for input.
It's the gateway to integration with CI/CD pipelines, automation scripts, and DevOps tools.
The --print (-p) flag: basic usage
The --print (or -p) flag launches Claude Code in non-interactive mode and outputs the result to stdout:
# Basic usageclaude --print "Explain what this function does"# With a file as context (piping)cat src/utils/auth.ts | claude --print "Identify security risks in this code"# Piping to other commandsgit diff HEAD~1 | claude --print "Write a CHANGELOG summary for these changes"# Shorthand -pclaude -p "Generate unit tests for the file src/lib/parser.ts"
Piping and scripts
The --print mode integrates naturally into Unix pipes:
#!/bin/bash# Pre-commit code analysis scriptCHANGED_FILES=$(git diff --name-only HEAD)for file in $CHANGED_FILES; doif [[ "$file" == *.ts ]] || [[ "$file" == *.tsx ]]; thenecho "Analyzing $file..."REVIEW=$(cat "$file" | claude --print \"Identify critical issues in this TypeScript code.Respond in JSON: {issues: array, severity: critical|high|medium|low}")echo "$REVIEW" | python3 -c "import sys, jsondata = json.load(sys.stdin)critical = [i for i in data.get('issues', []) if i.get('severity') == 'critical']if critical:print('BLOCKING:', critical)exit(1)print('OK: no critical issues')"fidone
--output-format json: automated parsing
To integrate Claude's output into scripts, use the JSON format:
# Structured JSON formatclaude --print --output-format json "Analyze this code and return a JSON object"# Stream JSON (for long outputs)claude --print --output-format stream-json "Generate the documentation for this API"
The json format returns an object with the following structure:
{"type": "result","subtype": "success","result": "The content of Claude's response...","session_id": "sess_abc123","total_cost_usd": 0.0023,"num_turns": 1}
Extract only the response with jq:
RESULT=$(claude --print --output-format json "Summarize in one sentence: $(cat README.md)" | jq -r '.result')echo "Summary: $RESULT"
--max-turns: limiting iterations
In headless mode, Claude can perform multiple "turns" (read files, write code, verify...). --max-turns limits this number to prevent infinite loops:
# Limit to 5 execution turnsclaude --print --max-turns 5 "Fix the TypeScript errors in src/"# For simple tasks, 1 turn is enoughclaude --print --max-turns 1 "Generate a README.md file for this project"
--dangerously-skip-permissions: full automatic mode
# In CI/CD only, in an isolated containerclaude --print --dangerously-skip-permissions \"Generate the missing tests for src/services/ and run them"
In production CI/CD, the recommended combination is:
claude \--print \--dangerously-skip-permissions \--max-turns 10 \--output-format json \"Your task here"
GitHub Actions integration
Full workflow: automated PR review
This workflow triggers a Claude Code review on every Pull Request:
name: Claude Code PR Reviewon:pull_request:types: [opened, synchronize]jobs:claude-review:runs-on: ubuntu-latestpermissions:contents: readpull-requests: writesteps:- name: Checkoutuses: actions/checkout@v4with:fetch-depth: 0- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: '20'- name: Install Claude Coderun: npm install -g @anthropic-ai/claude-code- name: Run Claude Code reviewid: reviewenv:ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}run: |DIFF=$(git diff origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' | head -c 10000)REVIEW=$(echo "$DIFF" | claude \--print \--max-turns 3 \--output-format json \"You are a code review expert. Analyze this Git diff and provide:1. A summary of the changes2. Potential issues (security, performance)3. Concrete improvement suggestionsFormat: Structured and concise Markdown." | jq -r '.result')echo "$REVIEW" > /tmp/review.mdecho "review_done=true" >> $GITHUB_OUTPUT- name: Post review commentif: steps.review.outputs.review_done == 'true'uses: actions/github-script@v7with:script: |const fs = require('fs');const review = fs.readFileSync('/tmp/review.md', 'utf8');github.rest.issues.createComment({issue_number: context.issue.number,owner: context.repo.owner,repo: context.repo.repo,body: `## Automated review: Claude Code\n\n${review}\n\n---\n*Automatically generated by Claude Code*`});
Workflow: automatic test generation
name: Generate missing testson:workflow_dispatch:inputs:target_path:description: 'Path to analyze (e.g. src/services/)'required: truedefault: 'src/'jobs:generate-tests:runs-on: ubuntu-latestpermissions:contents: writepull-requests: writesteps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: '20'- name: Install dependenciesrun: npm ci- name: Install Claude Coderun: npm install -g @anthropic-ai/claude-code- name: Generate missing testsenv:ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}run: |claude \--print \--dangerously-skip-permissions \--max-turns 15 \"Analyze the files in ${{ github.event.inputs.target_path }}.For each file without tests: create the Jest + TypeScript test filecovering nominal and error cases.Then run npm test to verify."- name: Create Pull Requestuses: peter-evans/create-pull-request@v5with:commit-message: "test: automatically generate missing tests"title: "Tests automatically generated by Claude Code"body: |Tests automatically generated for files without coverage.**Verify before merging:**- [ ] Tests cover nominal cases- [ ] Tests cover error cases- [ ] Coverage is > 80%branch: "auto/generated-tests"
Workflow: weekly security audit
name: Claude Code Security Auditon:schedule:- cron: '0 6 * * 1' # Every Monday at 6amworkflow_dispatch:jobs:security-audit:runs-on: ubuntu-latestpermissions:contents: readissues: writesteps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: '20'- name: Install Claude Coderun: npm install -g @anthropic-ai/claude-code- name: Run security auditid: auditenv:ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}run: |AUDIT=$(claude \--print \--max-turns 8 \--output-format json \"Full security audit:1. Search for hardcoded secrets2. Vulnerable dependencies (package.json)3. Dangerous patterns (eval, innerHTML)4. Security configurations (CORS, CSP)Return JSON with: critical (array), warnings (array), score (/10)" \| jq -r '.result')echo "$AUDIT" > /tmp/audit.jsonCRITICAL_COUNT=$(echo "$AUDIT" | python3 -c "import sys, jsontry:data = json.loads(sys.stdin.read())print(len(data.get('critical', [])))except:print(0)")echo "critical_count=$CRITICAL_COUNT" >> $GITHUB_OUTPUT- name: Create issue if critical findingsif: steps.audit.outputs.critical_count > 0uses: actions/github-script@v7with:script: |const fs = require('fs');const audit = fs.readFileSync('/tmp/audit.json', 'utf8');github.rest.issues.create({owner: context.repo.owner,repo: context.repo.repo,title: '[SECURITY] Critical issues detected by Claude Code',body: `## Security audit report\n\n\`\`\`json\n${audit}\n\`\`\``,labels: ['security', 'critical']});
Secrets and configuration variables
GitLab CI integration
Basic example of integration in a GitLab pipeline:
# .gitlab-ci.ymlstages:- review- testclaude-review:stage: reviewimage: node:20-alpineonly:- merge_requestsscript:- npm install -g @anthropic-ai/claude-code- |DIFF=$(git diff origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME...HEAD | head -c 8000)REVIEW=$(echo "$DIFF" | claude \--print \--max-turns 3 \"Review this diff and identify potential issues.")echo "$REVIEW"variables:ANTHROPIC_API_KEY: $ANTHROPIC_API_KEY
Pre-commit hooks with Claude Code
Integrate Claude Code into your git pre-commit hooks:
#!/bin/bash# .git/hooks/pre-commit# Make executable: chmod +x .git/hooks/pre-commitset -eSTAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx|js|jsx)$' || true)if [ -z "$STAGED_FILES" ]; thenexit 0fiecho "Claude Code analysis of staged files..."for file in $STAGED_FILES; doISSUES=$(cat "$file" | claude \--print \--max-turns 1 \--output-format json \"Identify CRITICAL issues (obvious bugs, security flaws).Respond with a JSON {issues: string[], has_critical: boolean}" \| jq -r '.result' 2>/dev/null || echo '{"has_critical": false}')HAS_CRITICAL=$(echo "$ISSUES" | python3 -c "import sys, jsontry:d = json.loads(sys.stdin.read())print('true' if d.get('has_critical') else 'false')except:print('false')")if [ "$HAS_CRITICAL" = "true" ]; thenecho "BLOCKED: critical issue in $file"exit 1fidoneecho "OK: no critical issues detected"exit 0
Security best practices in headless mode
Controlling costs in CI/CD
# Bad: sends the entire codebaseclaude --print "Analyze the entire project"# Good: target modified filesgit diff --name-only HEAD~1 | xargs -I{} sh -c \'cat {} | claude --print --max-turns 1 "Analyze this file"'# Good: limit diff sizegit diff | head -c 5000 | claude --print --max-turns 2 "Review this diff"
Next steps
Combine headless mode with enterprise providers (Bedrock, Vertex) for CI/CD pipelines that comply with your organization's data constraints.