Claude Code Permissions Best Practices: Headless Template A developer published a headless template for running Claude Code unattended, recommending that every reachable tool call resolve to an explicit allow or deny rule in settings.json rather than relying on prompts. The guidance pairs `claude -p` with `dontAsk` or `auto` mode and `--permission-prompts none`, reserving `--dangerously-skip-permissions` for disposable containers, and notes that permission rules evaluate deny first, then ask, then allow, with the first match winning regardless of specificity. Originally published at Unattended https://en.aobaworks.com/claude-code-permissions-best-practices-headless-template/?utm source=devto&utm medium=referral . For unattended Claude Code runs, the best practice is simple to state. Every tool call the job can reach should resolve to a firm allow or deny in settings.json . Nothing should be left to a prompt. Run with claude -p in dontAsk or auto mode, add --permission-prompts none , which the official docs recommend " when nobody is available to answer permission prompts, for example in a scheduled job https://code.claude.com/docs/en/headless ", and keep a deny list for destructive and outbound commands. Save --dangerously-skip-permissions for a disposable container. Start with a scoped allow list in dontAsk mode if this is your first overnight job. Move to auto mode once you trust the setup. Use bypass only inside a throwaway sandbox. | Your situation | What to pick first | First page to open | |---|---|---| | Beginner, first scheduled job | --permission-mode dontAsk plus a short allow list in .claude/settings.json | Configure permissions https://code.claude.com/docs/en/permissions | | Daily recurring work on a Pro, Max or Team plan | --permission-mode auto --permission-prompts none plus a deny list | Choose a permission mode https://code.claude.com/docs/en/permission-modes | | Tight budget or pay-per-token API key | dontAsk with --bare , so the job does only what you listed and loads nothing extra | Run Claude Code programmatically https://code.claude.com/docs/en/headless | | Job runs in a disposable container or VM with no secrets | bypassPermissions , with network restricted | Development containers https://code.claude.com/docs/en/devcontainer | | You need rules a glob can't express for example "only push to bot/ branches" | A PreToolUse hook script | Hooks reference https://code.claude.com/docs/en/hooks | Why dontAsk for beginners: it denies every call that would otherwise prompt https://code.claude.com/docs/en/headless , so the worst outcome of a missing rule is a skipped step. You never get a surprise action. Why auto for daily work: a classifier judges the long tail of commands you didn't foresee. The trade-off is less predictability. Claude Code permission rules have three outcomes, allow, ask and deny, and they're evaluated deny first, then ask, then allow https://code.claude.com/docs/en/permissions . The first match wins. On top of the rules, a permission mode decides what happens to calls no rule covers. The order is the key fact for unattended design. Specificity doesn't matter. A broad Bash aws deny blocks aws s3 ls even if you also allow that exact command. Ask beats allow in the same way: a matching ask rule prompts even when a narrower allow rule also matches permissions docs https://code.claude.com/docs/en/permissions . Some calls need no rule at all. In Manual mode config value default , file reads and Grep inside the working directory run without approval. So does a built-in set of read-only Bash commands. Edits, other shell commands, WebFetch and WebSearch need approval permissions docs https://code.claude.com/docs/en/permissions . The modes, as of October 2026 permission modes https://code.claude.com/docs/en/permission-modes , headless docs https://code.claude.com/docs/en/headless : | Mode | What runs without asking | Fit for unattended runs | |---|---|---| | default Manual | Reads only | In -p , unapproved calls are denied. Fine, but your allow list carries everything | | acceptEdits | Reads, file edits, common filesystem commands mkdir , touch , mv , cp | Good for lint/format jobs. Other shell and network calls still need allow rules | | plan | Nothing is executed; Claude proposes | Useful for a nightly "report only" job | | auto | Most actions, reviewed by a classifier model | Good for varied daily tasks | | dontAsk | Only what needs no approval, plus your allow rules | Most predictable choice for CI and cron | | bypassPermissions | Everything, including writes to protected paths like .git and .claude | Only in isolated containers/VMs | Two version notes matter for scheduled jobs. From v2.1.302, claude -p runs start in Manual mode when nothing sets a permission mode https://code.claude.com/docs/en/headless . From v2.1.283, auto mode became the built-in starting mode for interactive terminal and VS Code sessions https://code.claude.com/docs/en/permission-modes . The upshot is that a job you tested interactively may have run in a different mode than your cron job does. Always set the mode explicitly. Put permission rules under the permissions key in settings.json . List them in allow , ask and deny arrays of Tool or Tool pattern strings. Pick the file by who the rule is for settings docs https://code.claude.com/docs/en/settings : ~/.claude/settings.json : you, in every project .claude/settings.json : everyone in this repo commit it .claude/settings.local.json : you, in this repo only managed-settings.json : your organization, which overrides everything For a scheduled job, use the committed project file. The rules then travel with the repo to whatever machine runs the job. permissions object with defaultMode , allow and deny arrays. Leave ask empty for unattended repos. /permissions to see the rules Claude Code loaded This is an example to adapt. Swap npm for your own toolchain. { "permissions": { "defaultMode": "dontAsk", "allow": "Edit", "Bash npm test ", "Bash npm test ", "Bash npm run lint ", "Bash npm run lint ", "Bash git status ", "Bash git diff ", "Bash git log ", "Bash git add ", "Bash git commit " , "ask": , "deny": "Bash git push ", "Bash rm ", "Bash curl ", "Bash wget ", "Bash sudo ", "Read ./.env ", "Read ./.env. ", "Read ./secrets/ ", "WebFetch" } } Path syntax for Read and Edit follows gitignore rules. Each prefix means something different permissions docs https://code.claude.com/docs/en/permissions : | Pattern | Matches | |---|---| | //Users/alice/secrets/ | Absolute path from filesystem root | | ~/Documents/ .pdf | Path from your home directory | | /src/ / .ts | Relative to the settings file's project | | ./.env or .env | Relative to the current directory | A common mistake is writing Read /etc/passwd and expecting an absolute path. The single leading slash means "relative to the settings source". For absolute paths you need // . To run Claude Code unattended, use claude -p , set a mode explicitly, and add --permission-prompts none . Then make sure every call the job needs is covered by an allow rule. "Halting" happens in three different ways, and each has its own fix: claude instead of claude -p . -p in cron, CI and launchd. canUseTool callback, or an MCP tool passed via --permission-prompt-tool . Without --permission-prompts none , the docs say your run npm test , so it writes a polite summary and exits 0. This is the most common silent failure, and the fix is a complete allow list see the pre-flight section below . cd /path/to/repo && claude --bare -p "Run the test suite, fix failures, and commit the fix" \ --permission-mode dontAsk \ --permission-prompts none \ --settings .claude/settings.json \ --output-format json /var/log/claude-nightly.json Why each flag: --bare skips auto-discovery of hooks, skills, plugins, MCP servers, auto memory and CLAUDE.md. The docs call it " --settings . Also note that bare mode needs ANTHROPIC API KEY or an apiKeyHelper , because it .mcp.json servers will load. AskUserQuestion . It also tells Claude not to retry denied requests, so the run doesn't burn turns on them --output-format json gives you permission denials to review in the morning. For the scheduling side crontab entries, environment variables, logging , see Claude Code Cron Job Setup: Run Automations Overnight https://en.aobaworks.com/claude-code-cron-job-setup-run-automations-overnight/ . For jobs that died for reasons other than permissions, Why Claude Code Stops Mid-Automation and How to Fix It https://en.aobaworks.com/why-claude-code-stops-mid-automation-and-how-to-fix-it/ covers the rest. Choose dontAsk when the task is the same every night, like running tests or regenerating docs. Choose auto when the task varies and you can't list every command ahead of time. Auto mode has limits worth knowing. It needs a recent model: per the feature availability page https://code.claude.com/docs/en/feature-availability , that means Opus 4.6+, Sonnet 4.6+, Haiku 5.5, or a Fable model. In an interactive session, auto mode pauses and goes back to prompting after the classifier blocks 3 actions in a row or 20 in total. In a -p run, Claude Code doesn't stop the run https://code.claude.com/docs/en/permission-modes at those thresholds. Billing depends on setup. On Pro, Max and Team plans, classifier calls no longer count toward usage limits https://code.claude.com/docs/en/auto-mode-classifier-billing , but behind an LLM gateway or proxy Claude Code may make its own billed classifier requests. Check that page if you route through one. Auto mode isn't the right fit if you need exact reproducibility or an audit trail where "a model decided" won't do. For single-value keys like defaultMode , Claude Code uses this precedence: managed settings, then command line, then project local, then shared project, then user. Permission arrays don't override each other. They merge. From the settings docs https://code.claude.com/docs/en/settings : "A key set at a higher level overrides the same key set lower down." | Rank | Source | File / flag | Who sets it | |---|---|---|---| | 1 | Managed | managed-settings.json , MDM, or claude.ai console | Organization | | 2 | Command line | claude --settings , --permission-mode , --allowedTools | You, this run | | 3 | Project local | .claude/settings.local.json | You, this repo | | 4 | Shared project | .claude/settings.json | Everyone in the repo | | 5 | User | ~/.claude/settings.json | You, everywhere | The part that trips people up is arrays. When permissions.allow or permissions.deny appears in several files, Claude Code combines the lists https://code.claude.com/docs/en/settings . So a deny in your user file blocks an allow in the project file, and the reverse is true as well. Deny rules from every scope are checked before any allow rule. What this means for unattended runs: Bash npm allow sitting in your laptop's settings.local.json is often gitignored. --allowedTools adds; it doesn't replace. Write Claude Code Bash allow rules for specific commands and subcommands, such as Bash git commit , never a bare Bash or Bash git . How matching works permissions docs https://code.claude.com/docs/en/permissions , headless docs https://code.claude.com/docs/en/headless : matches one exact command. Bash npm test allows Bash git diff matches git diff --stat . Bash git diff also matches git diff-index . Bash safe-cmd does not allow safe-cmd && other-cmd . Each part split on && , || , ; , | , |& , & or a newline must match a rule on its own. Bash git status , Bash git add , Bash git commit . Bash git push origin bot/ . Better yet, push from your wrapper script after reviewing the result. Bash git push --force , Bash git reset --hard and Bash git clean in deny. That way a later broad allow can't open them up. | Mistake | What happens | Fix | |---|---|---| | Bash git | Allows git push --force , git clean -fdx , git config | One rule per subcommand | | Bash npm | Allows npm publish and npm install