Headless Claude Code Permissions Without Job Hangs
The answer: make unattended approval impossible to miss
Headless Claude Code permissions should be explicit, narrow, and non-blocking. In practice, that means starting scripted runs with claude -p, adding --bare when you want predictable automation, choosing a permission mode instead of accepting the default, pre-approving only the tools the job actually needs, and adding --permission-prompts none when no person or approval service can answer.
The goal is not silent broad trust. The goal is a job that either completes with known permissions, routes an approval to a policy host, or denies the unresolved action and exits in a way your harness can observe. A prompt hidden inside a detached terminal is not governance. It is a stalled build with a good excuse.
A safe baseline for locked-down CI looks like this: claude --bare -p "Run the test suite and fix any failures" --permission-mode dontAsk --allowedTools "Read,Bash(git diff *),Bash(npm test *)" --permission-prompts none --output-format stream-json --verbose. That pattern lets expected reads and test commands run, denies unapproved mutations, and gives the caller structured output to monitor.
Related reading: Claude Code permissions.
The failure mode: a job waits for a person who is not there
The common story is simple. A nightly worker starts Claude Code from cron. The prompt asks it to inspect a failing test, maybe patch a file, maybe run one command. The first few runs look fine, so the team wires it into a longer queue.
Then one night the model reaches an action that would normally ask for approval. In a developer terminal, that prompt is useful. In cron, systemd, a background pane, or a remote-control session, nobody sees it. The job appears hung. The queue backs up. The logs show activity until the moment they do not.
The fix is not to make every agent trusted by default. The fix is to decide what should happen before the prompt exists: allow the exact operation, deny it without waiting, send it to an approval broker, or run the whole thing in an isolated throwaway environment where broad permission is an acceptable trade.
Start with Claude Code print mode and bare runs
Claude Code print mode, claude -p "prompt", is the documented non-interactive entry point for scripts and CI/CD. It exits with 0 on success and a non-zero code on failures, which makes it fit normal automation wrappers.
For unattended work, Anthropic recommends --bare for scripted and SDK calls. Bare mode skips auto-discovery of hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory, and CLAUDE.md. That matters because discovery changes the tool surface. A CI job should not quietly inherit new project behavior because a repository file or MCP config appeared.
Without --bare, a -p run can load project hooks and .mcp.json servers even in untrusted folders without the same workspace trust dialog or per-server approval prompt you may expect from an interactive flow. If your platform needs repeatability, treat --bare as the default for automation unless you intentionally depend on those project assets.
Use permission modes as policy, not mood
In -p, the starting permission mode is Manual by default unless configured. Automation should pass a mode directly, because "whatever the default is here" is not a policy.
dontAsk is the most useful non-blocking mode for locked-down jobs. It denies every call that would otherwise prompt, while explicitly allowed tools and no-approval actions still run. This mode prevents a missing human from becoming an infinite wait, but it also means your allowlist must be accurate.
acceptEdits auto-accepts file edits and common filesystem commands in working directories and additional directories. That can fit controlled coding tasks, but it is broader than a read-only review bot or a build-log summarizer needs.
auto uses a background safety classifier for many actions. It can reduce prompt load, but it is still a decision gate, not an operating-system boundary. For production resources, credentials, deployment commands, and destructive operations, use explicit deny or ask rules around it.
bypassPermissions skips permission prompts, including protected paths. The CLI flag --dangerously-skip-permissions is equivalent to --permission-mode bypassPermissions. Anthropic says this belongs only in isolated containers or VMs. For sessions started with --bg, that mode persists after supervisor restarts, so treat it as a durable risk setting, not a one-off convenience.
Headless Claude Code permissions need denial fallbacks
The flag --permission-prompts none is the clean unattended answer when nobody can approve. Permission rules, the selected permission mode, and PermissionRequest hooks still decide first. Only otherwise-unresolved prompt requests are denied.
That detail is important. --permission-prompts none does not erase your policy. It gives unresolved prompts a terminal outcome. Claude is told nobody can approve and not to retry, so the run can fail in a controlled way instead of waiting on an SDK host or a prompt tool that is not there.
Pair it with narrow --allowedTools entries. Anthropic's example uses claude -p "Run the test suite and fix any failures" --allowedTools "Bash,Read,Edit". In production CI, you will usually want tighter Bash patterns than a broad Bash allow.
Approval brokers: SDK callbacks and MCP prompt tools
If you do have a policy service, use the Agent SDK canUseTool callback or the CLI --permission-prompt-tool. These are the headless replacements for a human clicking through a terminal prompt.
The SDK callback has one sharp edge: canUseTool is invoked only when permission evaluation resolves to a prompt. Calls already allowed by settings, rules, or mode do not go through it. If you need to inspect every tool call, Anthropic says to use a PreToolUse hook instead.
The MCP permission prompt tool has its own timing rule. Claude Code waits for that MCP server to connect before the first turn, up to MCP_TIMEOUT, which defaults to 30 seconds. That startup wait is useful when the approval broker is expected, but your harness should still decide what happens when the broker is absent or slow.
There is also a non-delegable class of MCP behavior. --permission-prompt-tool cannot approve MCP tools marked as requiring user interaction. Claude Code converts those allows to denies. If your background agent depends on such a tool, do not assume a prompt server can make it unattended.
Related reading: MCP security for coding agents.
Hooks are enforcement, callbacks are approval
Permission rules are enforced by Claude Code, not by the model. Their precedence is deny, then ask, then allow. Managed settings have the highest precedence, and organizations can disable bypassPermissions or auto through settings.
That order gives platform teams a useful split. Use managed settings for fleet-wide boundaries. Use deny rules for operations that should never happen in that environment. Use allow rules for the exact tools the job needs. Use SDK approval only for decisions that can genuinely be made at runtime.
When you need mandatory inspection of every tool call, use PreToolUse. The brief's field notes include a request for cleaner silent-deny behavior from hooks under scheduled or autonomous sessions, so treat hook behavior as something to test in your target Claude Code version, not as a checkbox.
Sandboxing still matters
Permissions and sandboxing solve different problems. Permissions decide which Claude Code tools, files, and domains can be used. Sandboxing is operating-system enforcement for Bash child processes.
That split matters most when teams are tempted by bypassPermissions. If you skip prompts, isolate the worker in a disposable VM or container with limited credentials and a disposable filesystem. A container with powerful cloud credentials is not a meaningful safety story just because the filesystem is temporary.
For deterministic review bots, avoid granting broad Bash just so Claude can inspect data. Pipe diffs, logs, or test output through stdin where possible. If the job can answer from supplied text, it does not need write access to the repository.
MCP adds a second permission surface
MCP servers expand what an agent can do, and they also expand what can go wrong. Claude Code uses MCP tool names in patterns such as mcp__server__tool. Project .mcp.json files and local MCP servers deserve the same scrutiny as scripts that run on a build worker.
The brief highlights MCP prompt-injection, credential, SSRF, and local-code-execution concerns. Those are not abstract when a background worker loads tools automatically. In unattended runs, prefer --bare unless MCP is part of the design, then allow only the specific MCP tools required for the job.
Field reports in the brief describe MCP approval prompts surfacing in the wrong place: a tmux teammate pane instead of the lead window, a local TUI prompt under systemd instead of a remote-control web UI, or a desktop terminal while a mobile remote session appears frozen. Treat those as caveats, not guaranteed current behavior, but design as if prompts can be missed.
Watch background work and final outcomes
In -p, background Bash tasks are terminated about five seconds after the final result and stdin closure. Background subagents and workflows keep claude -p open until they finish, with a default idle wait ceiling of 10 minutes through CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS.
On SIGTERM, Claude Code exits 143, terminates the Bash process tree, runs SessionEnd hooks, and leaves in-progress permission prompts unanswered. Resuming continues the unfinished turn. Your supervisor should account for that, especially if it retries after a hard timeout.
Use --output-format stream-json --verbose for long-running workers so your harness can capture tool activity, permission denials, and stalls before the final message. Add an external wall-clock watchdog anyway. One issue report in the brief describes intermittent stalls after tool_use with claude --print --permission-prompt-tool stdio, with recovery only through an external watchdog.
Practical patterns for CI and background agents
For locked-down CI, use --bare, --permission-mode dontAsk, a tight --allowedTools list, --permission-prompts none, streamed JSON, and an external timeout. Expect unapproved mutations to be denied. Make that failure visible in the build output.
For an ephemeral autonomous worker, use bypassPermissions only inside an isolated VM or container with scoped credentials and a disposable workspace. If the worker can reach production secrets, production cloud accounts, or durable shared storage, it is not isolated enough for broad bypass.
For an async approval broker, use SDK canUseTool or --permission-prompt-tool, then add --permission-prompts none as the fallback when the broker is absent or unresponsive. Put mandatory controls in hooks or managed settings, not only in the approval callback.
For deterministic build or review bots, send the relevant diff, logs, and test output through stdin. Give Claude read access only when supplied context is not enough. Add write tools only for jobs that are expected to patch files.
For long-running workers, capture the initialized tool surface, monitor background tasks, record permission denials, enforce wall-clock limits, and define resume behavior. A resumed unfinished turn can be useful, but only if your harness knows which step was interrupted.
Related reading: AI coding agent observability.
Field caveats to test before rollout
The official docs give the main controls, but the brief's issue reports point to liveness edges. One report says -p can hang or return no output when the model triggers an interactive clarification prompt. Another describes remote-control mobile sessions appearing frozen because permission widgets show only in the desktop terminal.
Other reports describe prompt placement problems in tmux teammate mode, remote-control MCP write prompts waiting inside a screen session under systemd, and hook outcomes that can still create scheduled-session deadlocks under dontAsk. These are community anecdotes and may be version-specific, so verify them against the Claude Code version you run.
The practical lesson is stable even if individual bugs move: do not rely on a human noticing a prompt in the right UI. Serialize policy where you can, stream what happened, and make denial a normal automated outcome.
A short rollout checklist
- Use
claude -pfor scripts and CI. - Add
--bareunless project hooks, memory, MCP, skills, orCLAUDE.mdare part of the intended runtime. - Pass an explicit
--permission-modefor every unattended run. - Prefer
dontAskplus narrow--allowedToolsfor locked-down automation. - Add
--permission-prompts nonewhen no approval host can answer. - Use
canUseToolor--permission-prompt-toolfor runtime approval, and hooks for mandatory enforcement. - Reserve
--dangerously-skip-permissionsfor isolated VMs or containers with limited credentials. - Use streamed JSON, hard timeouts, and clear resume rules for long-running workers.
- Test MCP tools, remote-control sessions, teammate mode, hooks, and clarification prompts in your actual version before relying on them.