Skip to content

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

The two-layer model of bounding permissions by structureLayer 1 is availability: whether the model even gets shown the tool in the first place. If it isn't shown, that's the end of it. Only if it is shown does it move to layer 2, where allow, deny, or ask-each-time applies. Deny rules always take precedence over allow rules.Tool callrequestedLayer 1: AvailabilityIs it shown to the model?--available-tools--excluded-toolsNot shown✕ Never used at allNo room to negotiateShownLayer 2: PermissionShown — but is it allowed?Has a deny — --deny-toolAllow only — --allow-toolUnspecified (default)Deny always beats allowBoth set on the same tool — deny winsResult: always denied / runs without asking / asks the human each time

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-all is set.

Permission can be scoped down with the syntax Kind(argument):

The --allow-tool and --deny-tool options accept permission patterns in the format Kind(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 shell rules, the :* suffix matches the command stem followed by a space, preventing partial matches. For example, shell(git:*) matches git push and git pull but does not match gitea.

shell
# 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.

json
"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

NameSource
Principle of least privilegePrimary — an established security principle. Not an agent-specific name
Bounding Permissions by StructureDescriptive — 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.