Permissions
Every tool call is checked before it runs. The check answers one of three things: allow it silently, deny it, or ask you. Which one you get is decided by a list of rules — and by which agent is running, because each agent brings its own defaults.
A rule
Three fields:
{
"permission": [
{ "permission": "bash", "pattern": "*", "action": "allow" },
{ "permission": "bash", "pattern": "rm *", "action": "deny" }
]
}permission— the tool id, or*for every tool. One exception:*does not coverbrowser_site, which is isolated on purpose so a broad browser grant can never silently authorise a real site.pattern— a glob matched against the call's value: the command forbash, the file path forread/edit/write.*matches any run of characters,?matches one, and matching is anchored at both ends and case-sensitive.action—allow,deny, orask.
The last matching rule wins. That is what makes the pair above work: the broad allow comes first, and the narrow deny after it overrides for the calls it matches. Reverse the two lines and the deny does nothing.
Where rules come from
Rules are merged in this order, and under last-match-wins a later layer overrides an earlier one.
- The agent's defaults. Built-in agents ship a ruleset —
buildasks before anything dangerous,exploreis read-only,compactionhas no tools. A custom agent defaults to* → ask. - Inherited rules, when this is a sub-agent — its parent's session grants. They sit here so the child's own later grants still win over what it inherited.
- Session grants. Every time you answer a prompt with "Always Allow", a rule is added for the rest of that session.
- Global config — the
permissionarray in~/.config/podcode/config.json. Applies everywhere. - Workspace config — the same array in
.podcode/config.json. Applies to that workspace only, and overrides the global one.
Edit layers 4 and 5 from Settings → Permissions, which tags each rule with its scope, or by hand in the files above.
Answering a prompt
When no rule decides, the tool call blocks and you get a dialog with three choices:
| Answer | Effect |
|---|---|
| Allow Once | Allow this call. Nothing is remembered. |
| Always Allow | Allow it and add a session rule, so the same shape of call stops asking. |
| Reject | Deny it. The agent is told, and continues without it. |
In the TUI these are a, b, and d.
The dialog shows the exact rule "Always Allow" will persist, which is not always the call in front of you — approving one git checkout may offer to persist git checkout *. Read it before clicking, since it's the rule you're granting.
Occasionally "Always Allow" isn't offered at all. That's deliberate: some asks persist nothing (a doom-loop check has to re-evaluate every repeat), and offering a standing grant the reply path would silently downgrade to allow-once would be a lie.
Rejecting one pending request can cascade-reject its siblings, so a batch of calls you don't want doesn't have to be refused one at a time.
Per-agent rules
An agent can patch its own defaults, in its frontmatter or in the config block. The shape is nested — tool, then pattern, then action:
---
description: Ships the build.
mode: primary
permission:
bash:
"*": ask
"npm run build": allow
agent_handoff:
"*": allow
---This is the form to reach for when a custom agent needs to run unattended. See Create an agent.
Credential files
Files whose contents are credentials get their own permission category, read_secret, rather than going through plain read. It covers .env and its variants, .npmrc, .netrc, .pgpass, credentials, id_rsa and the other SSH private keys, and anything ending .pem, .key, .p12, .pfx, .keystore, or .jks. Those files are also excluded by default from recursive searches and listings. Matching is on the filename, so ordinary files never touch this path.
The reason for a separate category is that a tool result is untrusted text going straight into the model's context. Reading a credential file is the first half of an exfiltration, and it should be a decision you make rather than a side effect of granting read.
The one permission no rule can silence anywhere is doom_loop: an "always" reply on it mints nothing and downgrades to allow-once, and even a wildcard allow in config won't suppress the interruption. An explicit deny still ends the loop immediately, which is the intended asymmetry.
Two more constraints worth knowing:
- Some agents get read-only bash.
plan,explore,review, andresearchdeny bash wholesale, with a carve-out: a command classified as read-only runs without a prompt, and anything that changes state is denied. They can inspect git history and the filesystem; they cannot act. - Sub-agents inherit by link, not by copy. A child spawned by
taskreads its parent's session grants live, so revoking a grant on the parent stops applying to a child that is already running.
Next
- Configuration — how the two config files merge.
- Create an agent — where a custom agent's default
* → askbites.