permissions.yaml
permissions.yaml is ForgeCode's policy file for built-in tools. It only matters when restricted mode is enabled in .forge.toml.
Start with the simplest possible example
This policy does three things:
- allows reads anywhere
- asks before writes
- blocks
rm
policies:
- permission: allow
rule:
read: "**/*"
- permission: confirm
rule:
write: "**/*"
- permission: deny
rule:
command: "rm*"
That is the whole mental model.
Turn it on
permissions.yaml does nothing until restricted mode is enabled:
restricted = true
Add that to ~/.forge/.forge.toml, or to the .forge.toml inside your custom config directory if you use FORGE_CONFIG.
When restricted = false, ForgeCode behaves normally and does not gate tool execution through this policy file.
Where the file lives
ForgeCode reads the file from its config directory:
- macOS/Linux:
~/.forge/permissions.yaml - Windows:
%USERPROFILE%\\.forge\\permissions.yaml - Custom config directory:
$FORGE_CONFIG/permissions.yaml
If restricted mode is enabled and the file does not exist yet, ForgeCode creates it with a default allow-all policy.
That default looks like this:
policies:
- permission: allow
rule:
read: "**/*"
- permission: allow
rule:
write: "**/*"
- permission: allow
rule:
command: "*"
- permission: allow
rule:
url: "*"
So turning on restricted mode alone does not make ForgeCode stricter. The restriction comes from the rules you write.
The shape of the file
Every file starts with one top-level key:
policies:
- permission: allow
rule:
read: "**/*.rs"
A simple policy has two parts:
permission: what should happenrule: what should match
Permission values
| Value | What it means |
|---|---|
allow | Run the operation immediately |
deny | Reject the operation immediately |
confirm | Pause and ask you first |
Rule types
A rule matches exactly one kind of operation:
| Key | Matches | Example |
|---|---|---|
read | File reads and file searches | "docs/**/*" |
write | Writes, patches, and deletes | "src/**/*" |
command | Shell command strings | "git *" |
url | Network fetches | "https://api.github.com/*" |
You can also scope any rule to a working directory with dir:
- permission: allow
rule:
write: "**/*.rs"
dir: "/home/user/project/*"
That rule only applies when the current working directory matches the dir glob.
How ForgeCode evaluates policies
A matching allow is not always final. ForgeCode can keep scanning because a later deny or confirm may still need to stop the operation.
Suppose ForgeCode wants to run git status.
run `git status`
-> check rules from top to bottom
-> matching deny? stop and reject
-> matching confirm? stop and ask
-> matching allow? remember it and keep going
-> nothing decisive matched? ask by default
That last line matters: no matching policy means confirm, not allow.
What ForgeCode actually checks
Built-in tools are mapped into four operation types:
| Tool family | Checked as |
|---|---|
Read, FsSearch | read |
Write, Patch, MultiPatch, Remove | write |
Shell | command |
Fetch | url |
Some tools are exempt from this policy system, including SemSearch, Undo, Plan, and Task.
MCP tools also bypass this file entirely. permissions.yaml governs ForgeCode's built-in tools, not external MCP integrations.
Confirmation mode
When a matching rule returns confirm — or when nothing matches and ForgeCode falls back to confirm — you get a prompt with three choices:
| Choice | Result |
|---|---|
| Accept | Allow this one operation |
| Reject | Deny this one operation |
| Accept and Remember | Allow it now and append a matching rule to permissions.yaml |
The remembered rule depends on what you approved:
| Operation | Generated pattern |
|---|---|
Read or write file.rs | *.rs |
Fetch https://example.com/api | example.com* |
Run git push origin main | git push* |
Run ls | ls* |
| Read or write a file with no extension | No rule is added |
This makes confirmation useful for tightening policies gradually instead of designing the whole file up front.
Logical policies
Simple rules cover most setups. When they do not, permissions.yaml also supports all, any, and not.
policies:
- all:
- permission: allow
rule:
read: "src/**/*"
- permission: allow
rule:
dir: "/home/user/project/*"
read: "**/*"
- any:
- permission: allow
rule:
read: "**/*.rs"
- permission: allow
rule:
read: "**/*.toml"
- not:
permission: deny
rule:
command: "rm -rf/*"
Use these sparingly. Most policy files are easier to reason about when each rule does one obvious thing.
Good patterns
Here are a few narrower patterns.
Allow writes only for one kind of file
policies:
- permission: allow
rule:
read: "**/*"
- permission: allow
rule:
write: "**/*.rs"
- permission: deny
rule:
write: "**/*"
Allow one API, deny the rest
policies:
- permission: allow
rule:
url: "https://api.github.com/*"
- permission: deny
rule:
url: "*"
Allow writes only inside one project directory
policies:
- permission: allow
rule:
write: "**/*"
dir: "/home/user/myproject/*"
- permission: deny
rule:
write: "**/*"
Common mistakes
Turning on restricted mode and expecting instant safety
Restricted mode only enables policy evaluation. If the generated permissions.yaml still allows everything, ForgeCode still allows everything.
Forgetting the fallback behavior
If no rule matches, ForgeCode asks. That is usually what you want, but it can feel surprising if you expected silent denial.
Trying to control MCP tools here
You cannot. This file covers built-in tools only.
Where to go next
If you have not enabled restricted mode yet, start with the .forge.toml setting in the .forge.toml reference.
Then come back and write the smallest policy file that matches how you actually work.