# Headless execution in Claude Code

Run claude -p from a GitHub Actions workflow when the agent-ready label is added, with pre-approved tools, a turn cap and a job timeout, then push agent/issue-<number> and open a pull request.

Source: https://ai-sw-factory.mellicci.dev/fundamentals/headless-execution/claude-code

## At a glance

| | Claude Code |
|---|---|
| Term | Print mode, `claude -p` (the Agent SDK as a CLI) |
| Configured in | CLI flags; `.claude/settings.json` and `~/.claude` are read too unless `--bare` |
| Loads / runs | Runs the loop once, prints the result to stdout, exits. Exit code 0 on success, non-zero on failure |
| Scope & precedence | `-p` starts in Manual mode, or auto where no flag, setting or feature-flag fetch decides, so pass `--permission-mode` and `--allowedTools`. Flags override settings files |

## Build the scenario

Two ways to run Claude Code in GitHub Actions: the `claude-code-action` or `claude -p` in a run step. This page shows `claude -p`, the primitive the action is built on, because the scenario needs a fixed branch name, a turn cap and your own use of the exit code.

| Step | What you write | Where |
|---|---|---|
| 1 | Secret `ANTHROPIC_API_KEY` | Repository settings, Actions secrets |
| 2 | Trigger, permissions, timeout | `.github/workflows/agent-ready.yml`, block 1 |
| 3 | Checkout, pnpm install, install Claude Code | Block 2 |
| 4 | Headless run, exit-code check | Block 3 |
| 5 | Push `agent/issue-<number>`, open the PR | Block 4 |

```yaml
name: agent-ready
on:
  issues:
    types: [labeled]
permissions:
  contents: write
  pull-requests: write
  issues: read
jobs:
  implement:
    if: github.event.label.name == 'agent-ready'
    runs-on: ubuntu-latest
    timeout-minutes: 20
```

```yaml
    steps:
      - uses: actions/checkout@v6
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: npm install -g @anthropic-ai/claude-code
      - run: |
          git config user.name "agent-ready"
          git config user.email "agent-ready@users.noreply.github.com"
```

```yaml
      - name: Run agent
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PROMPT: |
            Implement issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }}
            ${{ github.event.issue.body }}
            Run pnpm test, then commit when it passes.
        run: |
          claude --bare -p "$PROMPT" --max-turns 30 --permission-mode dontAsk \
            --allowedTools "Read,Edit,Bash(pnpm *),Bash(git status *),Bash(git diff *),Bash(git add *),Bash(git commit *)" \
            --output-format json > "$RUNNER_TEMP/result.json"
          jq -r '"cost \(.total_cost_usd) USD"' "$RUNNER_TEMP/result.json"
```

```yaml
      - name: Push and open PR
        env:
          GH_TOKEN: ${{ github.token }}
          N: ${{ github.event.issue.number }}
        run: |
          if git diff --quiet "origin/${{ github.event.repository.default_branch }}" HEAD; then
            echo "::error::agent made no commits"; exit 1
          fi
          git push origin "HEAD:refs/heads/agent/issue-$N"
          gh pr create --head "agent/issue-$N" --title "Agent: issue #$N" --body "Closes #$N"
```

The issue text reaches the shell only through the `PROMPT` environment variable, never pasted into the script. Claude gets file and pnpm tools plus four git commands; the workflow, not the model, pushes. Because the run step fails when `claude` exits non-zero, the push step never runs after a failed run.

## Specifics

### Running without a prompt

| Need | How |
|---|---|
| Run once and exit | `claude -p "<prompt>"` (`--print`) |
| Prompt from stdin | `cat build-error.txt \| claude -p "explain the root cause"`; stdin is capped at 10MB |
| Start faster, same result on every machine | `--bare`: skips hooks, skills, commands, subagents, plugins, MCP servers, auto memory and `CLAUDE.md` |
| Load context in bare mode | `--append-system-prompt-file`, `--settings`, `--mcp-config`, `--agents`, `--plugin-dir` |
| Credentials in bare mode | `ANTHROPIC_API_KEY` or `apiKeyHelper` via `--settings`; no OAuth or keychain |

Without `--bare`, `claude -p` shows no workspace trust dialog and no per-server approval, yet still runs the hooks in the project's `.claude/settings.json` and connects `.mcp.json` servers. Project `permissions.allow` rules are not used until the folder is trusted.

### Output and exit codes

| Item | Behavior |
|---|---|
| `--output-format` | `text` (default), `json` (one object with result, session ID, metadata), `stream-json` (one event per line, needs `--verbose`) |
| Exit code | 0 on success, non-zero when the run fails; 143 after SIGTERM. The documentation doesn't list other codes |
| Invalid flag | Error on stderr before the run starts |
| Failure inside the run (for example missing authentication) | Printed as the result on stdout |
| `subtype` | Documented for the Agent SDK's result message (`success`, `error_max_turns`, `error_max_budget_usd`, …); the CLI docs don't list it, so gate on the exit code |
| `--json-schema` | With `json`, puts schema-shaped data in `structured_output` |
| Cost | `total_cost_usd` and `session_id` in the JSON. A client-side estimate, not your bill. `--max-budget-usd` caps spend |

### Permissions when nobody can approve

| Flag | Effect |
|---|---|
| `--allowedTools` | Tools that run without a prompt, in permission rule syntax; `Bash(pnpm *)` matches any command starting with `pnpm ` |
| `--permission-mode dontAsk` | Anything that would prompt is denied. Reads and `--allowedTools` matches still run |
| `--permission-mode acceptEdits` | File edits and common filesystem commands run; other shell commands still need an allow rule |
| `--permission-prompts none` | Nobody answers: requests are denied and Claude is told not to retry (v2.1.259 or later) |
| `--max-turns` | Stops after N agentic turns and exits with an error |

A compound command such as `pnpm test && git add .` must match an allow rule for each part.

### Sessions and continuation

| Goal | Command |
|---|---|
| Continue the latest conversation | `claude -p "<next prompt>" --continue` |
| Continue a specific one | `claude -p "<next prompt>" --resume "$session_id"` |
| Get the ID | `claude -p "..." --output-format json \| jq -r '.session_id'` |
| Leave no transcript | `--no-session-persistence` |

A resumed run reports the conversation's whole cost, earlier runs included.

### CI integration

| Option | What it is |
|---|---|
| `claude -p` in a run step | This page: you own the branch, PR, limits and exit handling |
| `anthropics/claude-code-action@v1` | Official GitHub Action. A `prompt` input selects automation mode; `claude_args` carries CLI flags such as `--max-turns` and `--allowedTools`; `anthropic_api_key`, `claude_code_oauth_token` and `github_token` are the auth inputs. Its default auth needs `id-token: write` |
| GitLab CI/CD | A job in `.gitlab-ci.yml` that runs `claude -p` with a masked API key variable; beta, maintained by GitLab |
| Agent SDK | The same loop as a Python or TypeScript library, with callbacks and message objects |

## Gotchas

- `--bare` also skips `CLAUDE.md`, so ticket-service conventions don't load unless you pass them with `--append-system-prompt-file`. Without `--bare`, the run uses whatever hooks and `.mcp.json` the checked-out branch contains.
- Under `dontAsk`, a denied call doesn't fail the run. Claude continues without it, so check the diff, not only the exit code. With `stream-json`, denials appear as `permission_denied` messages and in `permission_denials` of the final result.
- Reaching `--max-turns` is an error, and the JSON has no `result` text. Use `--max-budget-usd` as a second guard on spend.
- Commits and PRs made with the default `GITHUB_TOKEN` don't trigger other workflows, so your test workflow may not run on the agent's PR. The documentation's fix is for the action and doesn't cover this workflow.
- The issue body is input from others. Keep the tool list narrow and read the [security model](https://ai-sw-factory.mellicci.dev/fundamentals/security-model) and [sandboxing](https://ai-sw-factory.mellicci.dev/fundamentals/sandboxing) pages before widening it.
