Copilot Assisted Permissions became generally available on October 26, 2026, in Copilot CLI, the GitHub Copilot app and VS Code. The idea: a language model looks at every approval request from the agent and waves through the clearly harmless ones on its own. Only the rest still reaches you.

I care about this topic because I know the alternative. If you let a coding agent work for an hour, at some point you approve every prompt without reading it. Or you start with --yolo right away. Both feel productive, and neither is. In this article we look at how the new mode decides, how to set it up, and which rules keep “fewer clicks” from turning into “less control”.

In short

Assisted permissions lets a review model approve clearly safe commands, file access and URLs, and only asks you when in doubt. That saves many clicks in daily work, but it replaces neither deny rules nor a sandbox. The biggest pitfall is combining it with --allow-all-tools in CI: if the mode cannot be enabled, the agent runs without any review.

What you will learn in this article

  • How the three approval modes in GitHub Copilot differ
  • In which order rules, earlier approvals and the review model decide
  • How to turn the mode on in the CLI, the app and VS Code
  • How to set firm guardrails with allow, ask and deny, and check them with TypeScript
  • What to watch out for when you use it in CI pipelines

What are Copilot Assisted Permissions?

A coding agent (an AI assistant that runs commands, edits files and fetches web pages on its own) needs permission for almost everything. GitHub Copilot now offers three modes for this. In manual mode, Copilot asks before every command that is not read-only, before every file edit and before every URL fetch. An approval can cover a single action or the whole session.

The opposite end is called “unconditional approval” in the docs and is better known as allow-all or YOLO mode. Copilot may then use tools, paths and URLs without prompts and without a model review. Deny rules and your company’s policies still apply; everything else goes through.

Assisted permissions sits right in between. A review model rates every eligible request in the context of your task. If it is confident that the action is harmless and fits the assignment, the action runs without a prompt. If not, Copilot asks you, just like in manual mode. The phrase “in the context of the task” matters: npm test is unremarkable while fixing a test, a curl to an unknown domain while renaming a variable is less so.

Three columns compare the Copilot approval modes manual, assisted and allow-all by prompts, review and use case

The three approval modes compared: assisted only asks when the review model is unsure.

Mode Who reviews? When does it make sense?
Manual You, for every action Unfamiliar repositories, work close to production, first steps with a new agent
Assisted Rules, then the review model, you when in doubt Daily work in your own repository with clear guardrails
Allow-all Only deny rules and company policies Throwaway environments such as containers or dev VMs without credentials

How does the review model decide?

The order is the key to understanding it. According to the GitHub docs, Copilot first checks existing rules and earlier approvals from the session. Anything already allowed runs without a prompt and without a model review. Anything that hits a deny rule is stopped. Only the remaining requests go to the review model, and only three kinds: running commands, accessing paths and fetching URLs.

After that there are three outcomes. If the model is confident, the action runs. If it is unsure, Copilot asks you. If the review cannot take place at all, for example because the service is unavailable, the session’s normal approval handling applies. In autopilot mode and in non-interactive runs, there is no human to ask. That is why uncertain requests are denied there, and Copilot receives the explanation so it can try another approach.

🎬 How an approval works with Copilot Assisted Permissions – this animated diagram only runs on my site: watch it on sascha-ms.de

What the review does not cover

You should know three limitations before you rely on the mode. First, the model leaves out further tool calls that an enterprise rule explicitly requires you to approve, such as certain MCP tools. Second, requests to bypass a local sandbox stay under normal handling. Third, GitHub states very clearly that the model can make mistakes and may approve dangerous actions too. Assisted permissions is explicitly not a replacement for a sandbox or access restrictions.

In addition, Copilot picks the review model itself. It can differ from the model you are currently working with. With a model provider of your own, your session’s active model handles the review.

Prerequisites

  • Copilot access where your company has not disabled the mode by policy
  • Copilot CLI (for example installed with npm install -g @github/copilot), the GitHub Copilot app, or VS Code with Copilot
  • For the check script below: Node.js 22 and TypeScript
  • A repository where you can experiment safely. A small, made-up library app is all you need

Set up Copilot Assisted Permissions step by step

Setup is quick. The real effort lies in the rules you build around it. So we go from switching it on, to the default, to a checked policy.

Step 1: Turn on the mode for one session

In an interactive CLI session, one slash command is enough. It applies only to the current session, and you can switch back at any time, even in the middle of a task.

/permissions assisted /permissions show /permissions manual 

/permissions show displays the active mode. Without an argument, /permissions opens a picker. Expected result: after switching, Copilot asks far less often for routine commands such as git status or npm test.

Step 2: Make assisted the default

To start new interactive sessions in assisted mode right away, set the key defaultPermissionMode in ~/.copilot/settings.json. Alternatively, use /settings during a session.

{ "defaultPermissionMode": "assisted" } 

Two details matter. If an administrator has set permissions.defaultMode, that value wins. And if the chosen mode is blocked by policy, the CLI starts in manual mode. Resumed sessions keep their saved mode.

Step 3: Copilot app and VS Code

In the GitHub Copilot app, open the mode menu in the prompt box, choose “Tool permissions” and then “Assisted”. This applies to the current session and also becomes the default for new local sessions. To change only the default, go to Settings, Sessions. Cloud sessions do not have this setting. In VS Code, pick “Assisted permissions” in the permissions picker.

Step 4: Guardrails with allow, ask and deny

Now comes the part that turns a convenience feature into a setting you can justify. With managed settings (a managed-settings.json that the company distributes via file, MDM or server) you define what is never allowed, what needs fresh consent and what is always allowed. Precedence is clear: deny beats ask, ask beats allow.

{ "permissions": { "disableBypassPermissionsMode": "disable", "defaultMode": "assisted", "deny": [ "Read(~/.ssh/**)", "Read(~/.aws/**)", "Edit(/.github/workflows/**)", "Shell(rm -rf *)" ], "ask": [ "Shell(git push *)", "Shell(npm publish *)", "Domain(api.github.com)" ], "allow": [ "Shell(npm test *)", "Shell(npx tsc *)", "Read(/src/**)", "Domain(registry.npmjs.org)" ] } } 

A few notes on the selectors: Shell(...) matches shell commands, where git push * works as a prefix and anything without an asterisk is compared exactly. Read(...) and Edit(...) use globs and know the roots // for the file system, / for the workspace, ~/ for the home directory and ./ for the current directory. Domain(...) controls network targets.

With disableBypassPermissionsMode you turn off YOLO mode without losing assisted. One interaction matters here: in the CLI, the review model can satisfy managed ask rules for shell, files and URLs. So an ask rule does not necessarily mean you will be asked. If you really want to decide on every git push yourself, move the rule to deny and push by hand.

Step 5: Check the policy automatically

Rule files grow, and at some point someone adds "defaultMode": "allow-all" for a quick test and forgets it. That is why an automatic check pays off. The small TypeScript module below reports typical gaps: unknown selectors, an allowed YOLO mode, missing blocks for ~/.ssh and ~/.aws, an unprotected git push, missing domain rules, and rules that never apply.

// src/policy.ts // Checks a Copilot permission policy (managed-settings.json) for common gaps. export type Severity = "error" | "warning" | "info"; export interface Finding { severity: Severity; rule: string; message: string; } export interface PermissionSettings { allow?: string[]; ask?: string[]; deny?: string[]; defaultMode?: "manual" | "assisted" | "allow-all"; disableBypassPermissionsMode?: "disable"; disableAssistedPermissionsMode?: boolean; } export interface CopilotSettings { permissions?: PermissionSettings; } const SELECTOR = /^(Shell|Bash|PowerShell|Read|Edit|Write|Domain)\((.+)\)$/; // Paths a coding agent should never read. const SECRET_PATHS = ["~/.ssh/", "~/.aws/"]; export function checkPolicy(settings: CopilotSettings): Finding[] { const findings: Finding[] = []; const p = settings.permissions ?? {}; const allow = p.allow ?? []; const ask = p.ask ?? []; const deny = p.deny ?? []; // 1. Every rule must use a known selector. for (const [list, rules] of Object.entries({ allow, ask, deny })) { for (const rule of rules) { if (!SELECTOR.test(rule)) { findings.push({ severity: "error", rule, message: `Unknown format in "${list}".` }); } } } // 2. The free-pass mode should be turned off. if (p.disableBypassPermissionsMode !== "disable") { findings.push({ severity: "warning", rule: "disableBypassPermissionsMode", message: 'Allow-all/YOLO is allowed. Recommendation: "disable".', }); } if (p.defaultMode === "allow-all") { findings.push({ severity: "error", rule: "defaultMode", message: "New sessions start without any review. Better: manual or assisted.", }); } // 3. Secrets in the home directory must be blocked. for (const path of SECRET_PATHS) { const covered = deny.some((r) => r.startsWith(`Read(${path}`)); if (!covered) { findings.push({ severity: "error", rule: `Read(${path}**)`, message: `Reading ${path} is not denied.`, }); } } // 4. git push should always need a fresh approval. const pushGuarded = [...ask, ...deny].some((r) => /^(Shell|Bash)\(git push/.test(r)); if (!pushGuarded) { findings.push({ severity: "warning", rule: "Shell(git push *)", message: "git push is in neither ask nor deny.", }); } // 5. Without domain rules the agent may request any URL it considers safe. const hasDomainRule = [...allow, ...ask, ...deny].some((r) => r.startsWith("Domain(")); if (!hasDomainRule) { findings.push({ severity: "info", rule: "Domain(...)", message: "No domain rules. A small allowlist makes network access traceable.", }); } // 6. Rules that are both allowed and denied: deny wins, the allow rule is dead code. for (const rule of allow) { if (deny.includes(rule)) { findings.push({ severity: "info", rule, message: "Listed in allow and deny – deny wins." }); } } // 7. Shell rules without " *" only match exactly. for (const rule of allow) { const m = SELECTOR.exec(rule); if (m && (m[1] === "Shell" || m[1] === "Bash") && !m[2].endsWith(" *") && m[2].split(" ").length < 3) { findings.push({ severity: "info", rule, message: 'Matches only this exact command. Append " *" for variants.', }); } } return findings; } 

Next, a lean command-line entry point that exits with code 1 on errors. That way you can plug it straight into a pipeline.

// src/cli.ts // Usage: node dist/src/cli.js managed-settings.json import { readFileSync } from "node:fs"; import { checkPolicy, type CopilotSettings } from "./policy.js"; const file = process.argv[2] ?? "managed-settings.json"; const settings = JSON.parse(readFileSync(file, "utf8")) as CopilotSettings; const findings = checkPolicy(settings); for (const f of findings) { console.log(`${f.severity.toUpperCase().padEnd(8)} ${f.rule} ${f.message}`); } const errors = findings.filter((f) => f.severity === "error").length; console.log(`\n${findings.length} findings, ${errors} of them errors.`); process.exitCode = errors > 0 ? 1 : 0; 

The tests run with the built-in Node.js test runner, without any extra library.

// test/policy.test.ts import { test } from "node:test"; import assert from "node:assert/strict"; import { checkPolicy } from "../src/policy.js"; const solid = { permissions: { disableBypassPermissionsMode: "disable" as const, defaultMode: "assisted" as const, deny: ["Read(~/.ssh/**)", "Read(~/.aws/**)", "Shell(rm -rf *)"], ask: ["Shell(git push *)", "Domain(api.github.com)"], allow: ["Shell(npm test *)", "Read(/src/**)", "Domain(registry.npmjs.org)"], }, }; test("a solid policy has no errors or warnings", () => { const f = checkPolicy(solid); assert.equal(f.filter((x) => x.severity !== "info").length, 0); }); test("empty settings report missing blocks", () => { const f = checkPolicy({}); const rules = f.map((x) => x.rule); assert.ok(rules.includes("disableBypassPermissionsMode")); assert.ok(rules.includes("Read(~/.ssh/**)")); assert.ok(rules.includes("Shell(git push *)")); }); test("allow-all as default is an error", () => { const f = checkPolicy({ permissions: { ...solid.permissions, defaultMode: "allow-all" } }); assert.ok(f.some((x) => x.rule === "defaultMode" && x.severity === "error")); }); test("unknown selectors are detected", () => { const f = checkPolicy({ permissions: { ...solid.permissions, allow: ["Exec(make)"] } }); assert.ok(f.some((x) => x.rule === "Exec(make)" && x.severity === "error")); }); test("exact shell rules get a hint", () => { const f = checkPolicy({ permissions: { ...solid.permissions, allow: ["Shell(npm test)"] } }); assert.match(f.find((x) => x.rule === "Shell(npm test)")?.message ?? "", /this exact command/); }); test("conflicts between allow and deny are reported", () => { const f = checkPolicy({ permissions: { ...solid.permissions, allow: ["Read(~/.aws/**)"] }, }); assert.ok(f.some((x) => x.rule === "Read(~/.aws/**)" && /deny wins/.test(x.message))); }); 

To run it, you need a package.json with "type": "module" and a tsconfig.json with "module": "NodeNext". Then these commands are enough:

npm install --save-dev typescript @types/node npx tsc node --test dist/test/*.test.js node dist/src/cli.js managed-settings.json 

For the sample policy above, the script reports no findings. An empty file, on the other hand, yields five findings, two of them errors, and exit code 1:

WARNING disableBypassPermissionsMode Allow-all/YOLO is allowed. Recommendation: "disable". ERROR Read(~/.ssh/**) Reading ~/.ssh/ is not denied. ERROR Read(~/.aws/**) Reading ~/.aws/ is not denied. WARNING Shell(git push *) git push is in neither ask nor deny. INFO Domain(...) No domain rules. A small allowlist makes network access traceable. 5 findings, 2 of them errors. 

Step 6: Assisted permissions in CI runs

Without a terminal, for example in a pipeline, you enable the mode with --assisted-approval together with -p, or with the environment variable COPILOT_ASSISTED_APPROVAL=true. Uncertain requests are denied here instead of prompted. On top of that, it pays to block pushing explicitly:

copilot -p "Fix the failing tests in src/" \ --assisted-approval \ --deny-tool 'shell(git push)' 

Watch out for the different syntax: on the command line the patterns are written as shell(git push) or shell(git:*), in managed settings as Shell(git push *). It is easy to mix them up.

Common mistakes and how to avoid them

Combining assisted with allow-all flags. --assisted-approval does take precedence over --allow-all-tools, but it changes nothing about --allow-all-paths and --allow-all-urls. Even trickier: if the mode cannot be enabled, for instance because of a company policy, the CLI only writes a warning to stderr. An additional --allow-all-tools then stays active, and the agent works without review. So simply leave the allow-all flags out in CI and read the output of your runs.

Treating ask rules as a hard boundary. As described above, in the CLI the review model can satisfy ask rules for shell, files and URLs. Anything that must never happen without you belongs in deny.

Mistaking exact shell rules for prefixes. Shell(npm test) only matches exactly that command, not npm test -- --watch. For variants, you need Shell(npm test *).

Using the flag interactively. If you start an interactive session with --assisted-approval, the CLI exits with an error. In interactive mode, use /permissions assisted or defaultPermissionMode.

Forgetting old approvals. Location-scoped approvals are stored in ~/.copilot/permissions-config.json, approved URLs in allowedUrls in the settings. Both persist across sessions. /reset-allowed-tools cleans...