Skip to content

Permission Rules

Permission rules tell Polytoken which tool calls to run automatically, which to ask you about, and which to block. A rule can match a tool name, a file path, a shell command, or a web URL.

For the configuration format (fields, types, YAML syntax), see the permission configuration reference. The Permissions page covers the interactive approval prompt.

Every time the model requests a tool call, Polytoken runs the built-in protections first, then gathers every rule that matches the call and resolves the matching rules into one verdict.

The protections guard your configured secret files and secret words in Standard and Autonomous modes. Bypass and Bypass+ skip the protections entirely, even though Bypass+ still evaluates your explicit deny rules. Each protection holds the call for your approval, filters secret material from the result, or denies the call outright. A hold lifts only when a rule names that exact command or, for file reads and grep, that exact path; broad allow rules do not lift a hold, and no rule lifts an outright denial. See Protecting secrets for which reads, commands, and searches each list affects.

Then Polytoken checks your rules. Rules live in four buckets that correspond to verdicts:

  • deny blocks the call immediately.
  • ask prompts you before running.
  • ask-unless-allowed prompts unless an allow rule of equal or greater specificity also matches the same call.
  • allow runs the call without prompting.

Deny beats ask, and ask beats allow. When multiple rules across different buckets match the same call, the most restrictive verdict wins. Within a single bucket, any matching rule is enough.

If no rule matches, the permission monitor decides what happens next.

A malformed rule can fail at parsing or at compilation, and Polytoken treats the two failures differently.

A rule with a syntax error, an unrecognized field, or an invalid value, such as command_tokens with no entries, fails while Polytoken parses the permission file. Polytoken rejects the whole file, whatever bucket the rule sits in, and the error names the file and the problem.

Polytoken judges a rule that parses but fails to compile, such as a glob with an unclosed [, by its bucket. In the allow bucket, Polytoken skips the rule and warns you with the rule name; other allow rules remain active, so one invalid allow rule does not prevent Polytoken from starting or reloading the permission configuration. In the ask, ask-unless-allowed, and deny buckets, a rule that fails to compile prevents startup with an error that identifies the rule and bucket. During reload, Polytoken keeps the previously active permission configuration instead of publishing the invalid ruleset.

The monitor controls what happens when no rule produces a verdict. You switch monitors with /permissions or the Ctrl+Shift+P typeahead.

Standard is the default. When no rule matches, Polytoken asks you. The approval prompt lets you allow the call once, for the session, for the project, or for your user account. Each choice takes effect immediately. The session choice lasts until the session ends. The project and user choices write the grant to a permissions.local.yaml file: the project choice in the project’s .polytoken/ directory, the user choice in $XDG_CONFIG_HOME/polytoken/ (default: ~/.config/polytoken/). When Polytoken cannot inspect a command, the prompt offers only one-time choices, with no option for the session, the project, or your user account.

Built-in protections still apply. Polytoken holds a command it cannot inspect (command substitution, nested shells, dynamic arguments) for your approval, because a rule whose conditions require particular command details cannot approve it safely. A whole-tool allow is the exception: it can approve the command without a prompt. A dynamic directory change such as cd "$WORKTREE" names no fixed destination, and the destination is the capability being granted. Polytoken holds such a change for your approval even when a whole-tool allow covers the tool.

Autonomous hands routine approvals to a classifier model. When a tool call would normally ask you, the classifier reads the request and decides whether to allow it. If the classifier is confident the call is safe, the call runs without interrupting you. When the classifier is unsure, Polytoken asks you instead. The fallback prompt carries the same scope choices as the Standard prompt whenever Polytoken can derive a permission rule for the call. Polytoken cannot derive a permission rule for an opaque shell command, a tool_flow script, or a dynamic directory change. The prompt for such a call offers only one-time choices: allow, deny, or allow with a permission-mode switch. Repeated classifier denials, which suggest the model is stuck, lead to the same one-time prompt.

Tool calls that match an explicit allow rule run immediately, without the classifier. Polytoken still asks you before granting access to a new filesystem location.

The plan facet can hand work to the execute facet in Autonomous mode.

Built-in protections still run before the classifier. Opaque shell commands go to the classifier, which reads the full command text and judges the command on that text. A literal relative directory change such as cd src names a fixed destination, so Polytoken can suggest permission rules for that directory. Polytoken holds a dynamic directory change for approval: the destination controls what the model can access, and Polytoken cannot know the destination before the command runs. Such changes include cd "$VAR" and any cd that follows a directory change Polytoken could not determine.

When the classifier approves a call and Polytoken can derive a deterministic permission rule for the call, the approval also creates a session-only overlay grant. The grant covers matching calls for the rest of the session. An approval with no derivable rule, such as an opaque shell command, covers only that one invocation. Overlay grants never persist to disk.

When Autonomous mode asks the classifier to assess a tool call, Polytoken sends the following inputs:

  1. The tool call, including the tool name and complete input JSON. For shell tools, the input also includes the full command and the permission subject under review.
  2. All configured permission rules and the deterministic candidate rule derived for the tool call.
  3. The session’s prior user prompts.
  4. The working directory for the tool call.
  5. The name of the facet the call runs in, together with that facet’s polytoken.autonomous_hint. For a call inside a subagent, the subagent’s definition takes the facet’s place: the classifier receives the subagent’s name and the subagent’s polytoken.autonomous_hint, plus a boundary note that the call runs inside a subagent.
  6. The authored autonomous_permission_matcher.classifier_rules value when you set it.
  7. Optional autonomous.md files from the user config directory and the project .polytoken/ directory.
  8. When a saved-session goal is running, the goal text you set with /goal. Subagent calls do not receive the goal text.

The classifier does not receive the facet’s prompt body, assistant replies, tool results, or system reminders. Polytoken sends only the user prompts from session history, not the other conversation messages.

Bypass runs every tool call without checking. Polytoken evaluates no rules, runs no protections, and shows no prompt. Use Bypass only when you want zero interruptions and accept full responsibility for what the model does.

Bypass+ skips most checks like Bypass, but still evaluates your explicit deny rules. If a deny rule matches, Polytoken blocks the call. Everything else runs without interruption. Use Bypass+ when you want an open mode but need certain paths, commands, or tools to stay off-limits.

Approvals you grant interactively build up across three scopes:

  • Session grants live in memory and disappear when the session ends. Every approval starts here unless you choose a broader scope.
  • Project grants persist in .polytoken/permissions.local.yaml inside the project directory. They apply every time you work in that project.
  • User grants persist in permissions.local.yaml under $XDG_CONFIG_HOME/polytoken/ and apply across all projects where you use Polytoken.

Polytoken reads rules from permissions.yaml as well as permissions.local.yaml in each location. The approval prompt writes its grants to the local file only, so permissions.yaml stays a file you edit yourself.

Seed permissions also run at session start. These built-in rules allow low-risk work without prompts: reading, writing, and editing files inside the project, changing directories within the project, running read-only shell commands like ls and cat, and using control-plane tools. The seeds also block direct edits to package-manager lock files, such as package-lock.json, with a message that tells the model to regenerate the lock file instead of editing it by hand. Seed rules do not persist and do not appear in your permission file.

Shell rules match the structure of a command, not its raw text. Polytoken parses each command into an executable, subcommand tokens, and flags, then matches each part separately. A rule that allows find does not allow the rest of the line.

Shell interpreter calls are exempt from structural matching. Commands that invoke bash, sh, zsh, or any other shell interpreter are always opaque, so command_tokens and executable rules cannot match them. A whole-tool allow can approve these commands silently. Without one, Polytoken asks you to approve each call one time in Standard mode, and the autonomous classifier judges the call in Autonomous mode.

Use command_tokens to match the beginning of a command as a prefix. A trailing "*" in the list makes the rule a prefix match: the actual command tokens must start with the listed tokens, and Polytoken allows anything after them.

allow:
- tool: shell_exec
args:
command_tokens: ["git", "status", "*"]

This allows git status, git status --short, git status ., and any other command that starts with git status. Without the trailing "*", the match is exact over the leading tokens Polytoken collects: the executable plus argument tokens up to the first token that looks like a flag, a path, a URL, a glob, or a number. command_tokens: ["git", "status"] therefore matches git status --short, because the flag never enters the token list, but not git status ., because Polytoken collects . as a third token.

Narrow the match to the executable name alone:

allow:
- tool: shell_exec
args:
executable: "rg"

Or combine the executable with its ordered subcommand tokens:

allow:
- tool: shell_exec
args:
executable: "git"
subcommand: ["remote", "get-url"]

The subcommand field matches only non-flag argument tokens, in order. The matcher treats a token as a flag when the token starts with - and has at least two characters, so a rule with subcommand: ["remote", "get-url"] matches git remote --verbose get-url as well as git remote get-url. The end-of-options marker -- counts as neither a flag nor a subcommand. The matcher does not track which flags take values, so a non-flag token that follows a flag still counts as a subcommand. For example, git commit -m msg produces subcommand: ["commit", "msg"], so a rule for subcommand: ["commit"] does not match.

The match requires the same number of non-flag tokens in the rule and the command. Use command_tokens with a trailing "*" when you want prefix matching instead.

The flags_present field constrains which flags the command may or may not carry. An array (or bare string) requires those flags to be present. To require their absence instead, use an object with an excludes key listing the flags that must not appear.

allow:
- tool: shell_exec
args:
executable: "find"
flags_present:
excludes: ["-exec", "-execdir", "-ok", "-okdir", "-delete"]

The rule allows find as long as none of the listed execution or deletion flags appear. It does not match find . -exec rm {} \; because -exec is present.

File tools match on path:

allow:
- tool: file_read
args:
path: "/src/**"

Polytoken treats bare strings with glob metacharacters (*, ?, [, {) as globs. ** matches any number of path segments. Use the {,/**} suffix to match both a directory and everything under it: /src{,/**} matches /src and /src/anything/inside.

To match a literal metacharacter, escape it with a backslash: path/to/\* matches a file literally named *, and nothing else. A character class such as [*] also matches a literal *.

Which characters trigger glob matching depends on the argument. Path arguments treat *, ?, [, and { as metacharacters. Flat arguments, such as the command_tokens of a shell rule, treat only * as a metacharacter; ?, [, and { are literal characters there and need no escaping.

A malformed glob, such as a rule containing an unclosed [, is an invalid rule. Polytoken skips an invalid allow rule with a warning, and an invalid rule in the ask, ask-unless-allowed, or deny bucket prevents startup. See Malformed permission rules for how Polytoken handles each bucket.

Polytoken resolves .. segments in a path before it matches the path against your rules, so a path like src/../README.md matches normally. A path that escapes through a leading .., such as ../secrets, remains blocked.

Web tools match on url:

allow:
- tool: web_fetch
args:
url: "https://polytoken.dev{,/**}"

The {,/**} suffix matches the address itself and every address under it. On URLs, the same rule also matches the address with a query or fragment attached: https://polytoken.dev?x=1 and https://polytoken.dev#section both match this rule.

Every rule can carry an optional message. When a rule with a message is the final verdict for a tool call, Polytoken injects the message text as a system reminder after the tool batch finishes.

Messages fire only for the resolved verdict. An allow rule with a message fires its message when Polytoken allows the call; a deny rule fires when Polytoken denies the call. Ask rules never produce messages. If multiple rules with messages match the same call, each one whose verdict matches the resolved outcome fires its message.

Use messages to explain why a rule exists or to guide the model toward a different approach. Bypass mode skips all messages.

Suppose you want Polytoken to ask before any git commit, but block git commit --no-verify outright. Commits that skip hooks are a common source of problems, so you want a message that tells the model why.

version: 2
ask:
- tool: shell_exec
args:
command_tokens: ["git", "commit", "*"]
deny:
- tool: shell_exec
args:
command_tokens: ["git", "commit", "*"]
flags_present: ["--no-verify"]
message: "All commits must go through commit hooks. Remove --no-verify."

Both rules match any command that starts with git commit. The deny rule only matches when --no-verify is present, and for those commands the call lands in the deny bucket. Deny outranks ask, so the verdict comes out deny:

  • git commit -m "fix" matches only the ask rule. The verdict is ask.
  • git commit --no-verify -m "fix" matches both rules. A deny match outranks the ask match, so the verdict is deny, and the message fires as a system reminder.

The flags_present: ["--no-verify"] form means the rule matches when --no-verify is among the flags Polytoken parsed from the command. Other flags on the line do not affect the match.

Two configuration lists guard sensitive material in Standard and Autonomous modes:

  • Secret files are paths you name as sensitive, such as .env. A file read that targets a secret file prompts you. A shell command prompts you when Polytoken recognizes a secret-reading pattern, such as cat or less opening the file. The grep tool runs without a prompt, and Polytoken filters from the results anything that would surface the path. Only a rule that names that exact command, or that exact path for file reads and grep, lifts the hold; broad allow rules do not. Polytoken denies two kinds of command outright, whatever your rules say: a command that directly prints a configured secret variable, and a command that reads environment secrets through a scripting-language interpreter such as Python or Node.
  • Secret words are values you name as sensitive. When a search would surface them, Polytoken filters them from the result before you or the model see them.

See the permission configuration reference for the secret_files and secret_words fields.