Configuring Claude Code’s dontAsk Permission Mode for a CI Pipeline, Step by Step

A padlock superimposed over a blue circuit board, representing locked-down CI pipeline permissions

dontAsk is the Claude Code permission mode built for unattended runs: it auto-denies any action that would otherwise show a prompt, and only lets through what you’ve explicitly pre-approved. For a CI pipeline, that means the setup work happens entirely before the run starts — in your allow rules, not in a dialog nobody is there to answer.

Step 1: don’t reach for bypassPermissions instead

It’s tempting to skip the rule-writing entirely and run CI with --dangerously-skip-permissions, which activates bypassPermissions mode. In this mode, Claude Code “disables permission prompts and safety checks so tool calls execute immediately, including writes to protected paths” — and .git and .claude are two of the directories on that protected list. Anthropic’s own documentation on permission modes carries an explicit warning against using it outside a disposable environment: “Only use this mode in isolated environments like containers, VMs, or dev containers without internet access, where Claude Code cannot damage your host system.” A shared CI runner with access to your deploy credentials usually isn’t that isolated environment. dontAsk gives you the same hands-off execution with an explicit allowlist instead of a blanket skip.

Step 2: write the exact rules your job needs

Start from what the job actually runs, not from a broad guess. If your CI step runs the test suite and nothing else, the rule set should say exactly that. In .claude/settings.json, at the repository root:

{"permissions": {"defaultMode": "dontAsk", "allow": ["Bash(npm test)", "Bash(npm run build)", "Read"], "deny": ["Bash(git push *)"]}}

Three things about this specific file matter more than they look like they would. First, Read with no parentheses matches every file read — fine for most CI jobs, since reads can’t modify anything, but worth narrowing to specific paths with Read(./src/**)-style specifiers if the repository holds files CI shouldn’t need to open. Second, Bash(npm test) with no trailing * matches only that exact command string — npm test --watch would still hit the default deny. Third, the explicit deny for git push isn’t redundant even though dontAsk already denies anything not allowed: a deny rule survives even if a later, broader allow rule gets added by mistake, because Claude Code evaluates deny before allow no matter what.

Step 3: get the wildcard placement right

A * in a Bash rule matches any text, including spaces, which makes it easy to write a rule far broader than intended. The permissions documentation’s own warning is specific: “put the * after the subcommand,” because Claude Code matches everything before the first * literally, and that’s what ends up limiting — or failing to limit — the rule.

You writeWhat it actually matchesThe mistake
Bash(git * main)Every git subcommand before main — git merge main, git push origin main, even git -c core.fsmonitor=<script> diff mainThe * sits before the subcommand, so it stands in for the subcommand itself, not for arguments after it
Bash(git log * main)Only git log variants: git log --oneline main, git log -5 mainNone — this is the corrected version, with the subcommand written out and the wildcard after it
Bash(ls*)ls -la and also lsofNo space before the *, so it matches partway into an unrelated command name
Bash(ls *)ls -la and bare ls, not lsofNone — the space before the trailing * is what excludes lsof

Claude Code does warn at startup about the clearest version of this mistake — an allow rule with a wildcard positioned before the rest of the command — but reviewing the rule against a table like this before it ships to a shared settings file is faster than waiting for a startup warning in a CI log nobody reads until something goes wrong.

Step 3b: lock out MCP tools you didn’t explicitly allow

If your CI environment has any MCP servers configured — even ones added for local development that happen to load in CI too — a CI job shouldn’t inherit access to all of them by default. Anthropic’s permissions documentation gives a deny rule for exactly this, using a tool-name glob rather than naming every server:

{"permissions": {"deny": ["mcp__*"]}}

That single rule removes every MCP tool from Claude’s context in that session, the same as a bare tool-name deny. If the job genuinely needs one MCP server — a deployment tool, say — allow rules for MCP tools work differently from the glob-everywhere pattern above: an allow rule accepts a tool-name glob only after a literal mcp__<server>__ prefix, so mcp__deploy-tool__* allows every tool from that one server by name, while an unanchored glob like a bare "*" in an allow rule is skipped with a startup warning and approves nothing. Narrow the exception to the one server the job needs rather than reopening the whole category.

Step 3c: verify the resolved rule set before trusting it

Settings merge from more than one file — a user-level file, a project file, and a managed-policy file can all contribute rules to the same session — so the file you edited isn’t necessarily the complete picture of what a CI job will actually run under. Before wiring a new allowlist into a pipeline that deploys anything, run the same command locally with --verbose once; Anthropic’s documentation notes this flag shows “the exact parameter names and values in each tool call,” which is also the fastest way to confirm a rule you wrote to match a parameter — such as Bash(run_in_background:true) — is actually matching the field name Claude Code sends, rather than a nested field the permission system can’t see at all, since only a tool’s direct top-level input fields are matchable this way.

Step 4: invoke it in the pipeline

With the rules in place, the CI command itself stays simple. Anthropic’s documentation gives this exact pattern for running in CI with a locked-down allowlist:

claude -p "run the test suite" --permission-mode dontAsk --allowedTools "Bash(npm test)" "Read"

--allowedTools on the command line adds to whatever your settings files already allow, so you can keep a job-specific allowlist in the CI config itself rather than editing .claude/settings.json for every job that needs a slightly different set of commands. If you’re running this inside Claude Code’s own cloud sessions rather than a self-hosted runner, note one platform-specific gap: cloud sessions ignore a dontAsk value set in a settings file, so the mode has to be passed as a flag on those runs instead of relying on a committed settings file to set it.

Step 5: know what fails silently instead of prompting

A handful of actions get denied under dontAsk even if you’ve allowed them elsewhere, and in CI this matters because there’s no human to notice a prompt was skipped — the job just gets a denial where an interactive session would have gotten a dialog. Per the documentation, this includes the built-in AskUserQuestion tool, MCP tools marked requiresUserInteraction, reads from network paths, and connector tools your organization has set to require approval. If a CI job relies on any of these, dontAsk isn’t the right mode for that step regardless of how the allowlist is written — the denial is enforced above the rule layer.

The practical check: run the job once locally with --permission-mode dontAsk and the same allowlist before trusting it in CI, and read the transcript for any denial you didn’t expect. A denial that traces back to one of these four categories means redesigning that step, not loosening the allowlist — loosening it won’t unlock a category dontAsk blocks unconditionally.

Making this the mandatory mode, not just the default

Everything above assumes a CI job that voluntarily sets --permission-mode dontAsk. Nothing stops a differently configured job, or a developer’s own script, from instead passing --dangerously-skip-permissions and running in bypassPermissions on the same shared runner. If the concern is organization-wide rather than one pipeline, the fix lives in managed settings rather than in any one job’s configuration: setting permissions.disableBypassPermissionsMode to "disable" in a managed-policy settings file removes bypassPermissions as an option everywhere that policy applies, and a parallel permissions.disableAutoMode setting does the same for auto mode. Both are explicitly described as most useful in managed settings specifically because, unlike a project’s own .claude/settings.json, a managed policy can’t be overridden by whoever writes the next pipeline config.

That distinction — a default a job chooses versus a policy a job can’t escape — is the real difference between “we usually run CI with dontAsk” and “CI on this repository cannot run with full permission bypass, full stop.” For a shared runner with access to deploy keys or production credentials, the second guarantee is the one worth setting up before the first pipeline goes live, not after an overly permissive job gets copy-pasted into three more.

For a complementary layer of control at the tool-call level rather than the mode level, blocking dangerous Bash commands with a PreToolUse hook covers rules that run even in modes other than dontAsk. If your CI setup runs several Claude Code sessions concurrently rather than one locked-down job, running parallel sessions with git worktrees is the related setup question. And if the goal is turning this into a named, reusable step rather than one-off CI config, building your first Claude Skill walks through packaging instructions and tools together. For scheduling the job itself outside a CI trigger, see scheduling a recurring Claude API call with cron.

Leave a Comment

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

Scroll to Top