# Claude Code Headless and Approvals: Unattended Without the Bypass Flag

In claude -p there is nobody to answer a permission prompt, so a tool call that would have asked is denied. The documented ways through are --permission-mode, --allowedTools, --permission-prompt-tool, or bypassPermissions. The reference: what each does headless, why a hook ask becomes a deny, and the shape that runs unattended without taking the gate off.

<div style="margin:8px 0 28px;padding:20px 22px;border:1px solid var(--line-2);border-radius:12px;background:var(--color-accent-light,#f0effe);">
  <p style="margin:0 0 12px;font-size:15px;line-height:1.6;color:var(--acp-text);"><strong>Just want the answer?</strong> Headless, a permission prompt has no answerer, so anything that would have asked is denied. Pre-decide instead: allow the tools the job needs, deny the ones it must never touch, and keep a hook on the path for everything else.</p>
  <pre data-track="CCHeadless: Hero Copy" style="margin:0 0 12px;background:var(--color-surface,#faf9ff);border:1px solid var(--line-2);border-radius:8px;padding:12px 14px;overflow-x:auto;"><code>claude -p "run the test suite and fix the failing test" \
  --permission-mode acceptEdits \
  --allowedTools "Bash(npm test:*)" "Bash(npm run lint:*)" "Edit" "Read" \
  --disallowedTools "Bash(git push:*)" "Bash(rm:*)"</code></pre>
  <p style="margin:0;font-size:13px;color:var(--acp-text-dim);"><a href="/personas" data-track="CCHeadless: Personas" style="font-weight:600;">Ready unattended policies (Fenced Worker) →</a> &nbsp;·&nbsp; <a href="/blog/interactive-vs-autonomous-the-empty-chair-test" data-track="CCHeadless: Empty Chair">the empty chair test →</a> &nbsp;·&nbsp; free up to 5 agents</p>
</div>

Every tutorial for running Claude Code in a pipeline reaches for `--dangerously-skip-permissions`, because the first unattended run stops on a permission prompt nobody can answer. The flag makes the stop go away. It also auto-allows everything the permission system would have asked about, which is the widest setting Claude Code has, applied at exactly the moment nobody is watching.

This is the reference for what actually happens to a permission prompt in `claude -p`, the four documented ways through, and the shape that runs unattended with the gate still on.

## Why this is structural, not a bug

A permission prompt is a question addressed to whoever is watching. Headless, by definition, nobody is. Claude Code's answer is the conservative one: in print mode the prompt "only exists when the Agent SDK's `canUseTool` callback supplies it," so a tool call that would have prompted is denied. That is the correct default, and it is why the first unattended run fails. The job asked a question with no one in the chair.

Codex CLI made the same call and cancels the tool call; [the Codex version of this page](/blog/codex-headless-mcp-approvals) covers it. The fix is the same on both: stop generating questions.

## The four ways through

**1. `--permission-mode`.** `default` prompts, which headless means deny. `acceptEdits` auto-approves file edits and common filesystem commands. `plan` reads before it writes and still prompts for the writes. `dontAsk` denies anything not pre-approved, which is the strictest headless setting and a good one for read-only jobs. `auto` runs a background classifier over actions instead of prompting. `bypassPermissions` auto-allows everything the permission system would have asked about.

**2. `--allowedTools` and `--disallowedTools`.** Standing answers to the questions you would have been asked. `Bash(npm test:*)` allows one command family; `Edit` allows a tool; `Bash(git push:*)` in the disallowed list denies one. Deny beats allow at every scope. These are the same rules as `permissions.allow` and `permissions.deny` in settings, passed on the command line.

**3. `--permission-prompt-tool <mcp_tool>`.** Names an MCP tool that gets called wherever a prompt would have appeared. Claude Code waits for that tool's server to connect before the first turn, up to `MCP_TIMEOUT` (30 seconds by default). The tool returns an allow or deny decision. It cannot approve an MCP tool that is itself marked as requiring user interaction.

ACP ships one. With the ACP MCP server connected (the installer registers it as `acp`), run:

```bash
claude -p "…" --permission-prompt-tool mcp__acp__acp_permission_prompt
```

Each prompt is evaluated against your policy at the background tier. Allow answers at once. Ask creates an approval, emails the workspace's owners with one-tap approve and deny links (and pushes to a subscribed browser), and holds the call while a human decides: 24 hours by default, set per agent type or tool with `approvalTtlSeconds` on the policy entry. Approved runs the call; denied, unanswered, or unreachable is a deny with a reason the model sees. The hold is real time on the job, so raise `MCP_TOOL_TIMEOUT` for the session if your harness caps tool calls shorter than your hold.

**4. The Agent SDK.** If the job is code rather than a shell line, the SDK's `canUseTool` callback is the prompt: your function receives the tool call and returns the decision.

## What a hook `ask` means headless

A `PreToolUse` hook can return `permissionDecision: "ask"` to force a prompt. In print mode there is no prompt to force, so the call does not run. Design for it: `ask` is a deny with a nicer name when nobody is on stdin.

This is the empty-chair rule, and it changes which policy you write. Attended, `ask` on `rm` is cheap: you look, you click. Unattended, `ask` on `rm` is a job that stops at the first cleanup step. The policy for an unattended run has to be answered in advance: the deletes it may do are `allow`, the ones it may never do are `deny`, and there is no third bucket. [The empty chair test](/blog/interactive-vs-autonomous-the-empty-chair-test) is the checklist.

## The shape that works today

1. **Pin the mode.** `acceptEdits` for jobs that write to the repo, `dontAsk` for jobs that only read. Not `bypassPermissions` for anything that touches a credential, a deploy target, or a real MCP server.
2. **Pre-decide the tools.** `--allowedTools` for what the job needs, `--disallowedTools` for what it must never do, or the same lists in the project's `.claude/settings.json` so they're reviewed with the code.
3. **Keep a hook on the synchronous path, and give the prompt an answerer.** Hooks fire in print mode and in every permission mode. A `PreToolUse` deny blocks the call with a reason the model sees and adapts to, with stdin closed. That is the gate that holds after step 2 misses a spelling. For the calls you want a human to decide, point `--permission-prompt-tool` at `mcp__acp__acp_permission_prompt`: the ask becomes an approval, the human answers from email or the console, and the job resumes or stops.
4. **Write the unattended policy as its own tier.** The ACP hook reads `permission_mode` and treats `bypassPermissions` as a background agent and `auto` as a subagent, so a rule written for the unattended tier is the one an unattended session gets. The [Fenced Worker persona](/personas#fenced-worker) is that policy ready-made: works freely, can't touch the shell, schedule itself, delegate, or read credentials.
5. **Fail closed when nobody is watching.** Interactive sessions fail open when the gateway is slow, with a loud line in the terminal. Unattended tiers stay blocked until the gateway answers, because a silent gap is the one thing an unattended run must not have.

With that in place the bypass flag is unnecessary, and if someone reaches for it anyway, the deny rules and the hook still hold under it; [the flag removes the prompts, not the hooks](/blog/claude-code-dangerously-skip-permissions).

## Troubleshooting

**The job stops with a permission error on a tool you expected to run.** It wasn't pre-approved. Add it to `--allowedTools` or the settings allow list; check the exact rule syntax (`Bash(npm test:*)`, not `npm test`).

**The job runs a tool you thought was denied.** Deny rules are string patterns over commands and [command strings have many spellings](/blog/claude-code-deny-list-bypass). Put the decision behind a hook that classifies the command rather than matching it.

**`--permission-prompt-tool` times out before the first turn.** The MCP server didn't connect within `MCP_TIMEOUT`. Raise it, or move the decision to a hook, which needs no server to be up.

**Everything is denied under `dontAsk`.** Working as designed: `dontAsk` only runs pre-approved tools. Add the allow list.

## Frequently asked questions



## Where to read more

- [Claude Code headless docs](https://code.claude.com/docs/en/headless) and [permission modes](https://code.claude.com/docs/en/permission-modes) &mdash; the upstream references
- [Run Claude Code in YOLO mode safely](/blog/run-claude-code-yolo-safely) &mdash; if you must use the flag, what to write first
- [Bypass permissions safely, across 19 agents](/blog/bypass-permissions-safely) &mdash; the same question for every harness
- [Claude Code hooks reference](/blog/claude-code-hooks-reference) &mdash; the PreToolUse contract in full
- [Codex exec and MCP approvals](/blog/codex-headless-mcp-approvals) &mdash; the sibling page for Codex
