A permission profile combines the filesystem and network boundaries into a named reusable preset. Instead of deciding each time by hand what the agent may read, write and where it may go over the network, you describe this once as a profile and apply it to a session or task. The built-in profiles - :read-only, :workspace and :danger-full-access - cover frequent cases, while your own are declared in the [permissions.<name>] section and selected via default_permissions or the permissions picker.
The filesystem part of a profile is built on special roots, and their semantics must be known. :minimal, :workspace_roots, :tmpdir and :slash_tmp are not arbitrary paths but named areas with a defined meaning. Inside the profile they and specific paths are assigned a level: read, write or deny. The key precedence rule is simple and cannot be bypassed by inattention: deny wins. A broad write does not cancel a narrow deny, so a ban on the sensitive stays even if writing is allowed nearby.
It helps to see a meaningful project-edit profile in full once. Below is a fragment: minimal read access, writing in the workspace roots, but an explicit deny on any .env, and the network off. It reads as an explicit description of working boundaries, not a set of random lines. You return to this form when setting policy for a context: the profile answers the question "what may the agent touch at all in this role" rather than deciding it anew on every action.
The network part of a profile is no less strict, and it is easy to get wrong on domain semantics. The network is enabled explicitly, and then domains are listed with an allow or deny decision. Importantly: an exact host matches only itself, *.example.com covers subdomains such as api.example.com but NOT example.com itself, while **.example.com takes both the apex and the subdomains. The difference is the apex, not nesting depth - and that is where people go wrong most often. A global * allows any public host and is valid in allow rules only. As in the filesystem part, deny wins, so a ban on an unwanted domain holds even if a broader pattern is allowed nearby.
It helps to see the network fragment of a profile too once. Below is an enabled network with a narrow allowlist of documentation domains and an explicit deny on an unwanted one. You return to this form when a task really requires the network: it is opened not wholesale but pointwise, for specific hosts. The difference between "enabled the network" and "enabled the network to these two domains" is the difference between a wide risk surface and a narrow, controlled boundary that can be explained in a review.
There are subtleties easy to forget that change the real boundary. Local and private targets are blocked separately - this is protection from requests into your own network, which should not be bypassed thoughtlessly. Unix sockets like the Docker socket are also a channel of impact, not a harmless file: access to it effectively gives control over containers. A profile is useful exactly to the extent you understand what each of its lines actually opens, not only what it allows on paper.
The point of profiles is reusing a conscious policy, not the convenience of copying. A named preset lets you apply the same verified boundary to many tasks and explain it in a review as a single whole. But copying someone's profile mechanically is dangerous: it allows exactly the paths and hosts another project needed. A profile is a template of structure filled with your values, understanding the cost of each line, not a ready allowlist for all cases.
The typical failures around profiles are predictable. Relying on a broad write to override a forgotten deny - though deny always wins. Writing *.example.com and being surprised that example.com itself is blocked, or reaching for ** and opening the apex along with it. Enabling the network wholesale instead of a narrow domain allowlist. And underestimating a Unix socket as a channel of impact. Build the profile as an explicit description of boundaries, remember that deny is stronger than allow, open the network pointwise and understand what each line actually opens.
default_permissions = "project-edit"
[permissions.project-edit.filesystem]
":minimal" = "read"
[permissions.project-edit.filesystem.":workspace_roots"]
"." = "write"
"**/*.env" = "deny" # deny wins over write
[permissions.project-edit.network]
enabled = false
# special roots: :minimal, :workspace_roots, :tmpdir, :slash_tmp[permissions.docs-check.network]
enabled = true
[permissions.docs-check.network.domains]
"developers.openai.com" = "allow"
"learn.chatgpt.com" = "allow"
"ads.example.com" = "deny"
# exact host matches only itself; *.example.com - subdomains, NOT example.com;
# **.example.com - apex and subdomains; a global * is allow-only; deny wins
# local/private targets are blocked separately; Unix sockets (Docker) - a channel of impact