Why Copilot CLI keeps asking for permission, and how to allow the right thing
GitHub Copilot CLI asks when an action is not covered by an approval you have already given. The prompt looks like one decision. It is not. A tool call, a file path, a URL, an enterprise policy, and the sandbox are separate gates. That is why a command can prompt again after you allowed it, and why --allow-all-tools can still leave a prompt on screen.
Read the prompt's wording first. A question about running a tool is the tool layer. A question about directory access, or a message that a path is outside the allowed directories, is the path layer. A URL question is the URL layer. A question about running outside the sandbox is the sandbox. A message about bypass permissions being disabled is policy. This page covers the CLI, including -p.
Run copilot --version before changing anything. The CLI ships weekly; 1.0.104 was current on 27 October 2026.
Checked against Copilot CLI's documentation on 27 October 2026.
Status, 27 October 2026: open reports cover a two-word command identifier never matching (#4150, confirmed on 1.0.98, updated 15 October 2026), shell(docker ps) failing (#2610), read(...) denying every read (#2722, updated 26 October 2026), allowed_directories not reaching the shell path gate (#4482, updated 2 October 2026), and assisted permissions expiring after about an hour (#4764). The mechanics below still apply if your version is newer; the reports may not.
The gates behind one prompt#
| Gate | What it decides | Check it with |
|---|---|---|
| Tool approval | May this tool, command, MCP tool, or memory write run? | the timeline, or the prompt wording; /permissions show reports the mode, not the approval list |
| Path | May Copilot touch this directory at all? | /list-dirs |
| URL | May Copilot fetch this host? | copilot config allowedUrls |
| Policy | Does an organization rule deny or ask for this action? | the startup warning or timeline; /settings Problems only rules out a malformed key |
| Sandbox | May the process reach this file or host? | /sandbox status, /sandbox policy |
MCP tools come from an MCP (Model Context Protocol) server, an external tool server the CLI connects to.
The permission mode is the session default: manual, assisted, or allow-all. Manual prompts for anything not already allowed. Assisted sends eligible requests to a model for a safety review and prompts when the review is unsure. Allow-all is unconditional. /permissions show prints the mode; /permissions manual, assisted, or allow-all changes it. The mode sets the default for tool requests, and allow-all also removes the path and URL gates. Even then, a deny rule, managed policy, or the sandbox can still stop a request.
Read-only work is usually allowed automatically, subject to policy. If a read prompts, check the path gate, a policy ask rule, or the sandbox before hunting for a missing read approval.
1. The rule names a different tool#
--allow-tool and --deny-tool take a kind: shell, read, write, url, memory, or an MCP server name. Shell commands are shell. The built-in view, glob, and grep are read tools, so shell(glob:*) never matches them (#1482, open). Allow read instead. An MCP tool uses the server's configured name, as in github(create_issue).
For shell command rules and saved commandIdentifiers, matching is literal. shell(git commit) matches that command. shell(git:*) matches git itself as well as git followed by a space and more text: git, git status, git push, but not gitea. A bare * means nothing in a saved identifier: git* does not match git status. Path rules use exact or trailing-path-segment matching, so write(README.md) matches any path ending in /README.md; the command reference also shows write(src/*.ts), so do not assume a general glob.
The trap is a multi-word command. shell(docker ps) failed in 1.0.21 (#2610, open), and any commandIdentifiers entry containing a space was ignored through 1.0.98 (#4150, open). Use a stem such as shell(docker:*) in a flag, or docker:* in commandIdentifiers. If that still prompts, it is the report, not your syntax.
Do not use read(...) to protect one file. On 1.0.27, any read deny pattern blocked every read, and shell(cat.env) bypassed it anyway (#2722, open). Use a sandbox denied path or a preToolUse hook (a command the CLI runs before a tool call), and treat the file's own permissions as the real control.
2. The path is outside the allowed list#
A tool approval is not a path approval. permissions-config.json, normally ~/.copilot/permissions-config.json, separates tool_approvals (what may run) from allowed_directories (where the path gate may reach). Approving write stops the tool prompt, but not a path prompt outside the working directory; adding a directory does not approve the write. The configuration reference spells out the split.
/list-dirs shows the allowed directories. /add-dir PATH allows one in the running session, and --add-dir=PATH does the same at startup. Adding a directory is also a trust decision: its .github/skills and .github/agents load as trusted configuration.
allowed_directories is the persistent form, but two reports say it is not applied to the shell path gate (1.0.79, #4482) or loaded at all (#4398). If /add-dir works and the next session asks again, put --add-dir in the launch command until it is fixed.
--allow-all-paths removes path verification entirely. It does not approve tools.
3. A URL is its own kind#
URL access is its own kind, url, with --allow-url, --deny-url, and --allow-all-urls. Patterns can be a host, a domain pattern, an exact URL (including a path), or a subdomain wildcard such as https://*.github.com. A permanent URL approval goes to allowedUrls in settings.json, not permissions-config.json, and applies across sessions and locations. deniedUrls wins.
Check copilot config allowedUrls. If the URL is there and the CLI still asks, you may be looking at the split-file report: in 1.0.45 the approval was written to settings.json but the runtime checked permissions-config.json for a URL entry (#3253, open). A command-line --allow-url is a baseline for that run, not a saved approval.
4. The approval was session-only, or saved for another location#
The prompt offers more than one kind of yes: allow once, or allow for the rest of the session. "Don't ask again in this repo" or "in this directory" is saved to the permissions file for the current location. --allow-tool and --deny-tool are startup flags: they apply to that session and are not saved. That is why yesterday's flag does nothing today.
The location key is the Git root, or the working directory outside a repository. Linked worktrees (separate checkouts of the same repository) share the main root; a submodule (a nested repository) uses its own directory. Renaming or moving the project breaks the key. COPILOT_HOME or --config-dir changes which file is read; the CLI uses only one, not a merge.
/reset-allowed-tools is more destructive than its name suggests. It clears session approvals, returns the mode to manual, and clears all saved approvals for the current location, including directories. Explicit startup flags such as --allow-tool and --deny-url remain the session baseline; it does not clear those. /permissions reset clears only in-memory approvals. To remove one bad entry, edit permissions-config.json while no session is running; the docs warn that editing it during a session can behave unexpectedly.
5. Deny and ask rules beat allow, including managed ones#
The order is deny, ask, allow. Deny beats allow even with --allow-all or a saved approval. Ask forces a prompt even when an allow rule matches.
Managed policy can set deny, ask, allow, limitTo, and permissions.disableBypassPermissionsMode. If any managed list is set, an operation that matches none of them defaults to ask. limitTo is a closed domain list that no user rule or allow-all mode can widen. Managed rules use capitalized Shell(...), Read(...), and Domain(...); flags use lowercase shell(...), read(...), and url(...).
disableBypassPermissionsMode: "disable" suppresses every allow-all flag and /permissions allow-all, but not assisted permissions. The refusal message says whether your settings or policy blocked it. An unrecognized value is enforced as disable, unconfirmed policy can block escalation, and managed settings refresh hourly. Check the startup warning or timeline, /permissions show for the mode, and /settings Problems for a malformed setting.
6. The sandbox is a second boundary#
Local sandboxing is off by default. When on, it puts shell commands and local MCP and language servers inside an operating-system boundary for filesystem and network access. It is separate from tool approval: an approved command can still be blocked. Built-in file tools run inside the CLI process and check the sandbox policy themselves, best-effort.
When a sandboxed command is blocked and sandbox.allowBypass is on (the default), the CLI offers to rerun it outside the sandbox, or to disable the sandbox for the rest of the session. Enterprise policy can make the sandbox a floor: --no-sandbox is then ignored, and sandbox.failIfUnavailable blocks rather than run unsandboxed.
Check /sandbox status and /sandbox policy; /sandbox policy npm install shows what one command would be allowed to reach. Do not disable the sandbox to fix a rule mismatch; add the path or approve one bypass.
7. In `-p`, nobody can answer#
Prompt mode, piped input, CI, and autopilot have no one to answer. A request that needs approval is denied, often with "Permission denied and could not request permission from user".
Give the run what it needs: --allow-tool='shell(npm:*), write', --add-dir, --allow-url. --assisted-approval turns on assisted permissions for the run: eligible requests go to the safety review, and anything that still needs you is denied with the reason passed back to the model. --allow-all-tools grants every tool; --allow-all or --yolo also removes the path and URL gates. Deny rules and policy still win.
For real logic, a permissionRequest hook sees the tool name and arguments and can return {"behavior":"allow"} or {"behavior":"deny"} before the permission service runs. It works in -p, does not run for read or hook kinds, and cannot pre-approve a sandbox bypass. The hooks reference documents the decision fields (behavior, message, interrupt); the input payload — the tool name and arguments — is not documented there, though issue reports such as #4150 show its shape.
When a correct rule still prompts#
If the kind, path, URL, saved location, policy, and sandbox all check out, you may be in an open report. Match its version to yours. The permissionRequest hook is the escape hatch: it sees the whole request, including a two-word command the matcher handles badly.
There is no --dangerously-skip-permissions flag in Copilot CLI. The nearest is --allow-all, which also removes the path and URL gates. Use it in an isolated environment, not as a fix.
Allow the right thing, not everything#
Start narrow: an exact rule, then a family rule such as shell(npm:*), then assisted permissions, then allow-all in a disposable environment. --available-tools and --excluded-tools change what the model can see, not what is approved; if you use both, the allowlist wins and the denylist is ignored.
Rules are enforcement; instructions are guidance. A rule that must always hold belongs in a deny rule, a sandbox path, or a hook, not only in AGENTS.md. The instructions guide covers the other half: why a file that loaded can still fail to change behaviour.
Where to go next#
- Copilot CLI guides — the rest of the set.
- Why Copilot CLI ignores your AGENTS.md — if the rule is in an instruction file rather than a permission.
- Why your Copilot CLI hook or plugin isn't firing — if the hook that should decide the request never runs.
- Context compaction — if a long session starts behaving differently after a summary.