# GitHub Copilot Permissions & Control Model, Explained

GitHub Copilot's controls across Copilot CLI, VS Code agent mode and the cloud coding agent: per-tool prompts and allow/deny flags, VS Code's permission levels and terminal auto-approve rules, the off-by-default sandbox, and a hook system that reads Claude Code's format. What each layer gates, where a false rule only prompts, and where an ask becomes a deny.

# The GitHub Copilot control model, explained

GitHub Copilot is three agents under one name: [Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli) in the terminal, agent mode in VS Code, and the cloud coding agent that works a GitHub issue into a pull request. They share an account and a subscription, and since 2026 they share a hook system, but their controls differ enough that the honest summary is three short models rather than one.

The reason this page matters more than its size suggests: Copilot is the harness most teams already have, because it came with the GitHub plan. When a team says it runs a mix of agents, Copilot is usually the one nobody chose.

*This page covers Copilot's own controls. For adding ACP to Copilot, see the [install guide](/integrations/copilot); for the cross-harness picture, [the controls comparison](/controls).*

## Copilot CLI

**Approvals.** Read-only tools — search, file reads, read-only shell — run without asking. Anything that can modify the system prompts: destructive shell commands, file edits, URL access. At the prompt you can allow the tool once, for the rest of the session, or in some cases "don't ask again in this repo", which is written to `~/.copilot/permissions-config.json` scoped to the git root. Permanent URL approvals go to `allowedUrls` in `~/.copilot/settings.json`.

**Rules.** Two layers on the command line. `--available-tools` and `--excluded-tools` decide which tools exist at all; then `--allow-tool` and `--deny-tool` decide what runs without a prompt, with a grammar that reaches into shell subcommands:

```
copilot --allow-tool='shell(git:*)' --deny-tool='shell(git push)'
copilot --deny-tool=write
copilot --allow-tool='read, write(.github/copilot-instructions.md)'
copilot --allow-tool='MyMCP(create_issue), MyMCP(delete_issue)'
```

These flags are session-only and are not saved. Deny wins unconditionally: GitHub's docs state that deny rules take precedence over allow rules "even when `--allow-all` is set", and over saved approvals.

**Escape hatch.** `--allow-all-tools` approves every available tool; `--allow-all` (alias `--yolo`) adds all paths and all URLs. `/allow-all` and `/yolo` do the same mid-session, and `/reset-allowed-tools` undoes it. On Copilot Business and Enterprise, an administrator can block the permissive options. The CLI also ships `/fleet`, which fans one prompt across several agents, and an autopilot that auto-responds to requests that would otherwise wait for you; [how the allow/deny rules apply to fleet-spawned subagents is not documented](/blog/who-controls-the-fleet).

**Sandbox.** None built in. GitHub's own advice before using the permissive options is to sandbox the CLI yourself.

## VS Code agent mode

**Approvals.** A permission level per chat session, defaulting to `chat.permissions.default`:

| Level | What runs without asking |
|---|---|
| **Manual** (default) | Whatever your tool, URL and terminal auto-approve settings already cover. Everything else prompts |
| **Assisted** (experimental) | Whatever an LLM judge approves. Requires `chat.assistedPermissions.enabled`; organizations can hide it |
| **Allow all** | Everything: file edits, terminal commands, external tool calls |

**Autopilot** is a mode rather than a level: it approves everything Allow all does, retries on errors, and answers the agent's own blocking questions. `chat.tools.global.autoApprove` removes prompts in every workspace, with a warning dialog the first time; the docs recommend the per-session level instead.

**Rules.** `chat.tools.terminal.autoApprove` is a map of command names or `/regex/` patterns to `true` (run) or `false` (prompt). Compound commands are matched per subcommand, and `false` always wins. The defaults auto-approve common read-only commands and prompt on `rm` and `del`. The sentence to remember is VS Code's own: a `false` rule "only forces a prompt; it does not block. To block, use a PreToolUse hook returning `permissionDecision: "deny"`." And: "Terminal auto-approval is a best-effort convenience, not a security boundary." The parser is tree-sitter, zsh and fish are read as bash, and obfuscation is acknowledged as a limit. Per-tool, `chat.tools.eligibleForAutoApproval` can pin any tool to manual approval.

**Sandbox.** Off by default on every platform. `chat.agent.sandbox.enabled` turns it on for macOS, Linux and WSL2 (bubblewrap and socat required on Linux); `chat.agent.sandbox.enabledWindows` for Windows, experimental and dependent on the September 2026 security update. It bounds terminal commands and their child processes only, not the file tools; writes are limited to the working directory; the network is open unless `chat.agent.sandbox.allowNetwork` is set to `false`. It stays in force under Allow all and Autopilot, and sandboxed commands are auto-approved by default. Organizations can require it through managed settings.

## Hooks, in both

Copilot's hook system is the layer that sees every tool call with its arguments, across every approval level, and the one place a call can be blocked rather than merely prompted. Eight events in VS Code (`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `SubagentStart`, `SubagentStop`, `Stop`); Copilot CLI adds `permissionRequest`, `errorOccurred` and `notification`. `PreToolUse` returns `permissionDecision` of `allow`, `ask` or `deny`, with a reason the agent sees on a deny.

| | Copilot CLI | VS Code agent mode |
|---|---|---|
| Locations | Admin policy files (`/etc/github-copilot/policy.d/*.json`, `C:\ProgramData\GitHub\Copilot\policy.d\*.json`), repo `.github/hooks/*.json`, user `~/.copilot/hooks/*.json`, repo `.claude/settings.json` and `.claude/settings.local.json`, plugins | Repo `.github/hooks/*.json`, repo `.claude/settings.json` and `.claude/settings.local.json`, user `~/.copilot/hooks`, user `~/.claude/settings.json`, plugins |
| Payload | Two formats, chosen by event-name casing: camelCase events carry `toolName` / `toolArgs`; PascalCase events carry snake_case fields with `tool_name` in Claude Code's vocabulary (`bash` → `Bash`, `view` → `Read`, `create` → `Write`) | Claude Code's field names, with VS Code's own tool ids (`run_in_terminal`, `create_file`, `replace_string_in_file`) and camelCase inputs |
| Decision | `permissionDecision` at the top level; `modifiedArgs` rewrites arguments | `hookSpecificOutput.permissionDecision`; `updatedInput` rewrites arguments, ignored if it fails the tool's schema |
| Matchers | Regex on the tool name, anchored | **Ignored.** Every hook runs on every tool |
| Exit codes | `2` denies, even if stdout said allow. Any other crash of a `preToolUse` command hook **fails closed**. Timeouts (default 30 s) **fail open**, "including `preToolUse` and admin-deployed policy hooks" | `0` parses stdout, `2` is a blocking error with stderr shown to the model, other codes warn and continue |
| Off switch | `disableAllHooks` in a hooks file or the repo settings; policy hooks still run | Organization policy can disable hooks |
| Status | GA | Preview |

Three consequences. First, Copilot reads Claude Code's format natively, so a repository that commits `.claude/settings.json` hooks for Claude Code already governs VS Code agent mode and Copilot CLI in that repo. Second, VS Code ignores matchers, so a hook must dispatch on `tool_name` itself. Third, the admin `policy.d` path gives an enterprise a way to deploy a hook to every CLI on a managed machine, and the timeout rule means that deployed hook still fails open when it is slow. GitHub's stated reason is the right one: "a slow or unreachable hook must not silently block tool calls or work."

Two open issues worth knowing before trusting a deny in a given version: a Copilot CLI release in which a `preToolUse` deny was not honoured ([copilot-cli#3874](https://github.com/github/copilot-cli/issues/3874)), and plugin-bundled `preToolUse` hooks not firing ([copilot-cli#2540](https://github.com/github/copilot-cli/issues/2540)). Repo and user hooks are the paths without a filed regression.

## The cloud coding agent

The coding agent runs in GitHub Actions and reads only `.github/hooks/*.json`. `preToolUse` fires; `permissionRequest` and `notification` do not; only `bash` entries run. GitHub documents that an `ask` is treated as a `deny` there. That is the correct posture and it is written down, which is rarer than it should be. What the sandbox lacks is any credential a hook could use to report home, so a hook that governs by consulting a policy service passes cloud runs through unless the repository provisions one.

## The empty chair

Three answers, one per agent. The cloud coding agent resolves an `ask` to `deny`, natively. VS Code under Autopilot answers its own questions, which is the point of Autopilot and the reason the sandbox stays on underneath it. Copilot CLI in a non-interactive run is not documented either way; the `permissionRequest` hook is offered as the tool for that case, which means the answer is whatever your hook says.

The gap is the same as everywhere else. Neither the CLI nor VS Code documents a durable per-tool-call record of what was allowed, prompted or denied. The decision happens and is gone. A hook that reports each decision to an independent record closes it, and Copilot's hooks are built to carry exactly that.

## Where Copilot sits

Against the [comparison table](/controls): a full native permission model in the CLI with an unusually good deny grammar and deny-beats-allow-always, a VS Code model whose rules prompt rather than block by its own admission, an off-by-default sandbox in VS Code and none in the CLI, a Claude-contract hook surface in both with one genuine enterprise pin (`policy.d`), and a documented headless posture for the cloud agent. The thing to know before trusting the defaults is that VS Code's terminal rules are a convenience layer, and its own docs point at hooks for the boundary.

## Frequently asked questions


