Bounding Permissions by Structure
Instructions get forgotten, but structure doesn't.
Problem
Write "don't touch production" in the instruction file, and the agent touches it anyway. Add "be careful" for emphasis, and it still executes a dangerous operation. The cause isn't defiance. It's that instructions aren't enforcement — the official docs themselves place the instruction file that way (context rather than enforced configuration, source in the Practice section). Instructions get read. But there's no guarantee they get followed. The real nature of this problem is that what you want kept is riding on a means with no guarantee of being kept.
Context
Applies when:
- An irreversible operation exists — production, billing, publishing, deletion
- The agent is given execution permission — not just reading, but writing, executing, sending
- It's an unattended path — a human isn't watching it live (→ Strong Gates for Unattended Paths)
Does not apply when:
- The work is read-only. There's nothing to bind
- Binding it too tight stops the work from getting done. Narrow permissions so far that a human has to approve every single step, and the benefit of automation disappears. This is a trade-off, not something where tighter is always better
Boundary with neighboring patterns:
- Strong Gates for Unattended Paths — that pattern is the approval and merge path, this pattern is execution permission. There's a junction, though: the official docs state plainly that a human can choose which tools an automation can use (You control which tools an automation can use, Practice section) — that's this pattern's own application inside an unattended path
- Separating Mechanical Gates from Semantic Gates — that pattern checks output (after the fact); this pattern stops it from happening in the first place (before the fact). Different points on the timeline
- Computation as Code — directly in tension. That pattern wants code executed; this pattern wants execution permission narrowed. What resolves it is allowing a narrow scope on the permission-pattern side (below)
- Rules Ranked by Binding Force — that pattern's limit is this pattern's entry point. Even writing MUST, instructions only ever work non-deterministically
Solution
Why does this work? Instructions and structure fail in different ways. An instruction fails by "not being read," "being misread," or "losing out to another instruction." Structure has no failure path at all — an operation without granted permission doesn't run, whatever the intent behind it.
So this pattern's claim isn't "structure is stronger." It's "structure doesn't depend on the model's state." Compliance with an instruction is swayed by the model, the context, the length — the official docs themselves state this non-determinism (Non-deterministic behavior, Practice section). Permission isn't swayed. Whether the agent is sharp or sloppy, confused or not, it can't step outside its permissions. What this pattern does is convert a trust problem into a form that doesn't need trust.
The diagram's key point is that the two layers are separate things. Layer 1 (availability) decides whether something gets shown at all. A tool that isn't shown can't possibly be used. Layer 2 (permission) decides, once shown, whether to allow it, deny it, or ask. "Usable, but needs permission" and "doesn't exist at all" are different — the former leaves room to negotiate, the latter leaves none. Evaluation order matters too: deny always takes precedence over allow (Deny rules always take precedence over allow rules, Practice section).
There's one more correspondence: with irreversibility. Binding everything is expensive. What should get bound narrows to "what can't be undone if it goes wrong." A reversible operation is covered well enough by a gate applied after the fact (→ Separating Mechanical Gates from Semantic Gates). The cost of binding by structure can be discounted by reversibility.
This pattern also includes freezing and diff-only reference. Forbidding writes to a primary source or a finalized artifact isn't an independent mechanism — it's just one application of least privilege. Make only the range that's allowed to change writable, and leave everything else read-only, and a diff simply has no way to originate from outside the permitted range.
Trade-offs
Advantages
- Doesn't depend on the model's state. Whatever the agent's condition or however its interpretation wavers, an operation outside its permissions doesn't happen
- Sets a ceiling on the damage from an irreversible operation. Outside the bound range, whatever happens can be undone
Costs
- The tighter it's bound, the more work stalls. Waiting for approval piles up, and the benefit of automation thins out. An operation that asks about everything erases the point of automating at all
- Designing permissions depends on understanding what the work needs to do. Exploratory work where what's needed isn't known ahead of time can't be bound correctly — it tends to end up as either binding too tight and stalling constantly, or getting annoyed and opening everything up
GitHub Copilot in Practice
Copilot isn't a single product. The mechanism corresponding to this pattern differs by surface.
Copilot CLI — two-layer tool permissions
Source: CLI command reference (retrieved 2026-07-16) Official.
Layer 1 (availability — is it shown at all):
|
--available-tools=TOOL ...| Only these tools will be available to the model. For multiple tools, use a quoted, comma-separated list. … | |--excluded-tools=TOOL ...| These tools will not be available to the model. For multiple tools, use a quoted, comma-separated list. |
Layer 2 (permission — shown, but is it allowed):
|
--allow-tool=TOOL ...| Tools the CLI has permission to use. Will not prompt for permission. For multiple tools, use a quoted, comma-separated list. … | |--deny-tool=TOOL ...| Tools the CLI does not have permission to use. Will not prompt for permission. For multiple tools, use a quoted, comma-separated list. |
Evaluation order:
Deny rules always take precedence over allow rules, even when
--allow-allis set.
Permission can be scoped down with the syntax Kind(argument):
The
--allow-tooland--deny-tooloptions accept permission patterns in the formatKind(argument). The argument is optional—omitting it matches all tools of that kind.
|
read| File or directory reads |read,read(.env)| |shell| Shell command execution |shell(git push),shell(git:*),shell| |url| URL access via web-fetch or shell |url(github.com),url(https://*.api.com)| |write| File creation or modification |write,write(src/*.ts)|
For
shellrules, the:*suffix matches the command stem followed by a space, preventing partial matches. For example,shell(git:*)matchesgit pushandgit pullbut does not matchgitea.
# Allow all git commands except git push
copilot --allow-tool='shell(git:*)' --deny-tool='shell(git push)'Don't conflate these
This line is from the tool permission pattern section. A similarly worded statement exists elsewhere for deniedUrls (a config key), but that one is about URL access. Don't mix the two into a general "Copilot always denies first." What can be stated here is the evaluation order for tool permissions only.
This one line embodies this pattern's core — "git is fine to use, but not push." Written in structure, not instruction. write(src/*.ts) is a direct implementation of "freezing and diff-only reference" (forbidding writes to a primary source and a finalized artifact). Narrow the writable range with a glob, and leave everything else read-only.
github.com — instructions only ever work non-deterministically
GitHub's docs for code review custom instructions state this plainly (source: Using custom instructions to unlock the power of Copilot code review, retrieved 2026-07-16, Official. This quote describes code review custom instructions and isn't extended as a benchmark for instruction files in general):
Non-deterministic behavior: Copilot may not follow every instruction perfectly every time.
The reason "don't touch production" in an instruction file is no guarantee is stated here in the official docs' own words. This is exactly why an "irreversible operation" needs this pattern's permission, not an instruction.
cloud agent — network and tool control
Source: Copilot cloud agent risks and mitigations (retrieved 2026-07-16) Official.
To mitigate this risk, GitHub restricts Copilot cloud agent's access to the internet.
This is as far as the official docs state. The allowlist details, org policy, and repo settings are unconfirmed and not written here.
For automations that run unattended, choosing the tools themselves is given as structure:
You control which tools an automation can use. When an automation is triggered by an event, input from untrusted users could become part of the prompt. To limit the impact of prompt injection, you choose exactly which tools an automation can use, so it can only take the actions the task requires.
The less a path is watched by a human, the more it's worth narrowing which tools are allowed ahead of time. Against the approval-and-merge structure that Strong Gates for Unattended Paths covers, this sentence is a real instance of this pattern at work inside it.
VS Code — sensitive files require approval
Source: Reviewing AI-generated edits (retrieved 2026-07-16) Official.
To prevent inadvertent edits to sensitive files, such as workspace configuration settings or environment settings, VS Code prompts you to approve edits before they are applied.
"chat.tools.edits.autoApprove": {
"**/*": true,
"**/.vscode/*.json": false,
"**/.env": false
}Default to auto-approving, and carve out only sensitive files — not "bind everything," but this pattern's own design of narrowly binding only what can't be undone or shouldn't be seen.
Contrast with Claude Code
The instruction file's status is the same outside Copilot too. Claude Code's official docs write this Official (source: code.claude.com/docs/en/memory, retrieved 2026-07-16):
Because they're context rather than enforced configuration, how you write instructions affects how reliably Claude follows them. Specific, concise, well-structured instructions work best.
The status of instructions as context, not enforced configuration, holds regardless of tool. This is the core of why this works, and the reason a separate layer — permission — is needed.
Also Known As
| Name | Source |
|---|---|
| Principle of least privilege | Primary — an established security principle. Not an agent-specific name |
| Bounding Permissions by Structure | Descriptive — this catalogue's descriptive name |
Least privilege predates agents, and this pattern's claim itself isn't new. What's new is where it gets aimed. Because an agent appears to be following natural-language instructions, the temptation is strong to control it through instructions rather than permissions. But instructions, as above, are "context, not configuration." This pattern is an established principle, re-aimed at a target where that temptation runs especially strong.
Related
- Strong Gates for Unattended Paths — that pattern is the structure of the approval and merge path, this pattern is execution permission. The junction point is an automation's tool control (Practice section)
- Separating Mechanical Gates from Semantic Gates — that pattern checks output (after the fact), this pattern stops it from happening at all (before the fact). Different points on the timeline
- Computation as Code — directly in tension. That pattern wants code executed, this pattern wants execution permission narrowed. Resolved by allowing a narrow scope on the permission-pattern side
- Rules Ranked by Binding Force — that pattern's limit is this pattern's entry point. Even writing MUST, instructions only ever work non-deterministically
- Failure as Institutional Memory — shares the contrast of binding with structure instead of writing "be careful"
- Diff Review and Incremental Adoption — edits to sensitive files can be put behind approval (Practice section)
- If reading just one more next: Separating Mechanical Gates from Semantic Gates — for what's left over once structure can't bind it, this covers which gate should watch it