Currently Available: Need a skilled Software Developer for your next project?
Categories
Coding Agents

How to Run Claude Code Headless in CI Without Hanging on Permission Prompts

When a continuous integration (CI) job runs Claude Code, no person is usually available to approve tool use. Claude Code’s -p option runs the CLI in non-interactive mode: it accepts a prompt without opening the usual conversation in a terminal. But -p does not tell Claude Code what to do when it wants to use a tool that requires approval.

For an unattended job, set a permission mode explicitly, allow only the tools the task needs, and tell Claude Code not to wait for a permission handler. The job should also check that the requested work actually happened, because a denied tool call does not necessarily make the CLI exit with an error.

Set permissions so the run fails closed

A permission prompt asks for approval before Claude Code uses a tool or performs an action. In a normal interactive session, a person can answer it. In CI, the run can stall if a configured approval handler, the component that answers permission prompts, does not respond. If no handler is configured, Claude Code may deny the action and continue, leaving the requested work incomplete.

For a direct CLI job, this pattern makes unattended behavior explicit:

claude --bare -p \
  --permission-mode dontAsk \
  --permission-prompts none \
  --allowedTools "Read,Edit,Bash(npm test:*)" \
  --max-turns 8 \
  --output-format json \
  "Make the requested change and run the tests"

dontAsk denies actions that would otherwise require approval, while allowing actions that do not need approval and actions covered by applicable allow rules. --permission-prompts none tells Claude Code not to consult or wait for a permission handler. The example permits reading, editing, and Bash commands matching the supplied test pattern; replace those permissions with the narrow commands your job actually needs. The programmatic-use documentation lists --permission-prompts none for Claude Code v2.1.259 and later.

The allowlist does not block every action. Claude Code can take some actions without approval even when they are not on the list. Test the policy against the actual task, and use runner restrictions such as limited credentials or network access to enforce boundaries outside Claude Code’s permission checks.

Choose auto instead of dontAsk when you want Claude Code's classifier, a component that reviews proposed actions, to assess them. Pair it with --permission-prompts none so Claude Code denies an unresolved request instead of waiting for a person or handler. With auto, the classifier helps decide which actions to allow; the mode does not act like a fixed allowlist. Avoid bypassPermissions or --dangerously-skip-permissions as a routine fix for CI hangs: those options skip permission checks rather than defining a bounded set of allowed actions. The permission-mode documentation describes these modes and their tradeoffs.

Check the result, not just the exit status

A permission denial and a failed CLI run are different outcomes. Claude Code may continue after it denies a tool call, so a successful process exit does not prove that the requested change or test occurred. A GitHub issue about denied permissions in headless runs reports inconsistent visibility of some denials; treat it as a warning about observability, not as a guarantee about every version or platform.

The CLI supports JSON output, which gives a script structured data to inspect. For example:

set -euo pipefail

result_file="$(mktemp)"
trap 'rm -f "$result_file"' EXIT

claude --bare -p \
  --permission-mode dontAsk \
  --permission-prompts none \
  --allowedTools "Read,Edit,Bash(npm test:*)" \
  --max-turns 8 \
  --output-format json \
  "Make the requested change and run the tests" >"$result_file"

jq -e '.is_error == false and .subtype == "success"' "$result_file" >/dev/null

# Verify the task's result independently.
npm test

Do not treat this JSON check as a rule for every permission denial. Confirm the response fields for the CLI version you pin. Then add a check tied to the task: run the relevant tests, verify an expected file change, or validate the generated artifact. The CLI reference also documents --max-turns to limit how many turns Claude Code takes and --max-budget-usd to set a client-side cap on estimated spending. Add a CI job timeout as a separate safeguard, especially when the run uses background work.

Use --bare only for self-contained tasks

--bare reduces automatically loaded configuration, which makes a scripted run less dependent on the CI runner’s local setup. It skips automatic discovery of items such as project and user instructions, hooks, skills, plugins, MCP servers, and memory. Hooks do not run in bare mode.

Use it when the prompt and command-line configuration provide everything the task needs. If the task relies on project instructions or an MCP server, supply the needed context through supported options or run without bare mode and test the project’s configuration in the CI workspace. For Anthropic API authentication, bare mode requires ANTHROPIC_API_KEY or an apiKeyHelper; subscription login credentials do not authenticate the run in this mode. The headless-use documentation describes these limits.

A command that Claude starts in the background can keep a headless run open after Claude has finished its main turn. The documentation gives continuous idle waiting a 10-minute default cap, but a still-running background command can keep the run open until that command exits or reaches its own limit. Bare mode disables background tasks, which can help when the job should stay simple and deterministic.

Pass permissions deliberately in GitHub Actions

The official Claude Code Action runs Claude Code with its own configuration and authentication, separate from a direct claude -p command. The Action accepts CLI arguments through claude_args. For example:

- uses: anthropics/claude-code-action@v1
  with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    claude_args: |
      --allowedTools "Bash(npm install),Bash(npm run test),Edit"
      --permission-mode auto
      --permission-prompts none

Here, auto lets a classifier review many actions, the listed tool rules define permitted commands, and --permission-prompts none prevents an unresolved approval request from waiting on a handler. Check that the CLI version used by the Action supports the flags you pass, including the v2.1.259 minimum for --permission-prompts none. The Action’s configuration documentation explains its command permissions.

Permission flags do not replace GitHub workflow security. Give the workflow only the GitHub token permissions it needs, and take care with workflows triggered by untrusted pull requests. The Action’s security guidance warns about exposing base-repository secrets through pull_request_target or workflow_run workflows. For authentication, the Action also supports Workload Identity Federation, which exchanges a GitHub Actions OpenID Connect (OIDC) token for a short-lived Anthropic access token; its setup documentation explains the required configuration.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Email updates

Usually a new article and a few links I found interesting.

No spam. Unsubscribe with one click.

Leave a Reply

Your email address will not be published. Required fields are marked *