Permissions in the command line are described by short tokens, and that is a good decision: the policy reads as a list of actions rather than a paragraph of prose. Five types cover everything the agent can do outward: a shell command, reading a file, writing a file, a network request and calling an external server's tool. A denial is always stronger than an allowance, and relative paths are counted from the workspace - absolute ones can point beyond it.
A naive policy looks generous: allow everything that might be needed so nothing gets in the way. It turns the configuration into decoration. Permission for any network request and any write means there is effectively no policy, only a line that creates a feeling of control. The value of a list is its narrowness: it must name specific commands, specific paths and specific addresses.
It helps to lay the token types out in a table with examples once. Below is that map: a command with an optional argument pattern, reading and writing by path masks, a network request by domain, an external server's tool by name. Note the difference between a command's name and the pattern of its arguments - that is the same word-boundary technique as in other tools: without it a permission spreads to commands that merely start the same way.
| Token | Example | What it controls |
|---|---|---|
| Shell(command) | Shell(git), Shell(curl:*) | The command name and an optional argument pattern |
| Read(path) | Read(src/**/*.ts) | Reading files |
| Write(path) | Write(package.json) | Creating and modifying files |
| WebFetch(domain) | WebFetch(docs.github.com) | Network requests to an exact domain or a pattern |
| Mcp(server:tool) | Mcp(linear:search*) | Specific tools of an external server |
It also helps to see a complete policy block. Below are permissions for working with git, the package manager's tests, reading and writing inside the sources, reaching one documentation domain and one external server tool; the denials cover reading environment files, writing keys and deletion. Such a block can be explained in review line by line, and that is the main criterion of a good policy.
A wildcard is authority, not convenience. A permission of the form any network address, any tool of any server, write anywhere or arbitrary arguments for a download utility opens a data path outward. That is exactly what a typical hole looks like: it is not created by malice but appears from a wish not to return to the configuration. The right order is to list the real commands and addresses first and widen as necessity is proved.
The precedence of a denial over an allowance is worth using deliberately rather than treating it as a technical rule for resolving conflicts. Allowances describe work and are therefore inevitably broad: the sources as a whole, every command of the version control system. Denials describe data that nothing should touch: environment files, keys, directories with dumps. Narrowing a broad allowance is hard and easy to forget, while a single denial acts on top of all current and future allowances - and that is more reliable.
A separate subtlety is relative versus absolute paths. Relative ones are tied to the workspace, and that makes the policy portable between machines. Absolute ones can point beyond it, which is sometimes needed - for example to a neighboring package of schemas - but every such entry deserves its own decision: it widens the scope past the boundary of the project the policy was written for, and on another machine it may point somewhere entirely different.
A policy grows in one direction: from a refusal to an allowance. The agent hits a denial, you see a specific command and add a line for it rather than for a class of similar ones. Every line has a reason, and while the reason is remembered the policy is alive. The sign that it has started to rot is a line whose origin nobody can explain: people are afraid to delete it in case something breaks, so it stays forever, widening rights for no reason.
The engineering conclusion is simple: a permission policy is code that gets read. It should fit on a screen, consist of clear lines and be explainable in a couple of words each. If a policy cannot be read in a minute, it will not be reviewed, which means that within a month it will stop matching reality and turn into a set of historical allowances.
The typical failures are predictable. Opening the network entirely instead of listing domains. Allowing a download utility arbitrary arguments and getting a path outward. Mixing project permissions and personal exceptions in one list. And writing a policy nobody can explain line by line.
{
"permissions": {
"allow": [
"Shell(git)",
"Shell(pnpm:test*)",
"Read(src/**)",
"Write(src/**)",
"WebFetch(cursor.com)",
"Mcp(linear:search*)"
],
"deny": [
"Read(.env*)",
"Write(**/*.key)",
"Shell(rm)"
]
}
}