Permissions and security guards
Every built-in permission string and every guard's tier
The mental model is at /concepts/permissions-model. This page lists the strings.
Built-in roles
Roles form a strict tower: each role bypasses every guard at its tier and below.
| Role | Default match[] | Tier bypass cap | Default permissions[] |
|---|---|---|---|
owner | [{ kind: 'tui' }] | high | channel.respond, session.control, session.admin, cron.schedule, cron.modify, subagent.spawn, subagent.cancel, subagent.output, subagent.spawn.operator, fs.see.private, fs.see.secrets, security.bypass.low, security.bypass.medium, security.bypass.high, plus every plugin-contributed security.bypass.<guard> (wildcard expansion) |
trusted | [] | medium | channel.respond, session.control, session.admin, cron.schedule, subagent.spawn, subagent.cancel, subagent.output, subagent.spawn.operator, fs.see.private, fs.see.secrets, security.bypass.low, security.bypass.medium |
member | [] | low | channel.respond, session.control, subagent.spawn, subagent.cancel, subagent.output, fs.see.private, security.bypass.low |
guest | [] (fallback) | (none) | (none) |
User-declared permissions[] replaces the built-in list. "permissions": [] means no permissions, including no channel.respond.
The audience-leak defense (owner-in-public-channel) moved from the language default to operator config: scope roles.owner.match[] tightly. Default match is TUI-only, where a human is present. Configs that widen owner to a channel author re-open audience-leak for that author and should remove security.bypass.high (and the wildcard sentinel) from roles.owner.permissions[] for those origins.
guest carries no permissions by default, but it is grantable — an operator may add channel.respond to guest to let strangers drive masked turns. The hard fail-safe floor is therefore not the guest role but the undefined origin: a call with no resolvable actor holds nothing, regardless of what guest is granted. session.control is split out from channel.respond for exactly this reason — a respond-capable guest can talk but cannot abort other speakers' sessions (/stop gates on session.control, which stays member-and-up by default).
You don't have to hand-edit typeclaw.json#roles to grant a role. From the TUI or a 1:1 DM, an owner or trusted user can ask the agent to grant roles conversationally via the grant_role tool — see Runtime role grants below.
Core permissions
| Permission | Granted by default to | Effect |
|---|---|---|
channel.respond | owner/trusted/member | gate on the channel router; without it, inbound messages are dropped before session creation |
session.control | owner/trusted/member | control session lifecycle, e.g. /stop; split from channel.respond so a respond-only guest can't abort other speakers' sessions |
session.admin | owner/trusted | operate the agent from a channel: /reload (config + subsystems) and /restart (bounce the container); above session.control because both drop every in-flight session |
cron.schedule | owner/trusted | allow scheduling new cron jobs at runtime |
cron.modify | owner | allow editing existing cron jobs |
subagent.spawn | owner/trusted/member | allow spawning subagents |
subagent.cancel | owner/trusted/member | allow cancelling in-flight subagents |
subagent.output | owner/trusted/member | allow reading subagent output |
subagent.spawn.operator | owner/trusted | allow spawning write-capable (operator) subagents |
fs.see.private | owner/trusted/member | see the private working surface (workspace/, memory/, sessions/); without it those dirs are hidden from bash and the file tools |
fs.see.secrets | owner/trusted | see the credential files (.env, secrets.json); without it they are masked |
security.bypass.low | owner/trusted/member | tier-wide bypass for low-severity guards |
security.bypass.medium | owner/trusted | tier-wide bypass for medium-severity guards |
security.bypass.high | owner | tier-wide bypass for high-severity guards |
security.bypass.<guard> | varies | per-guard bypass; see tier tables below |
Plugin-contributed permissions live under the plugin's namespace (<plugin>.<verb>.<noun>). They're declared on definePlugin({ permissions: [...] }) and surface in roles.<name>.permissions[] once the plugin is loaded.
fs.see.private / fs.see.secrets are phrased as capabilities to see so the tower stays monotonic: a role without them has the matching paths hidden from both bash (bwrap masks) and the file tools (the privateSurfaceRead guard). A custom role can carry them explicitly, e.g. "contributor": { "match": ["slack:T0 author:U_C"], "permissions": ["channel.respond", "fs.see.private"] } sees the working surface but not the secret files. The agent folder's top-level public/ directory is never hidden by either grant — it is the one zone a guest can read and write. See the sandbox internals for the enforcement details.
Security guards — high tier
Audience-leak severity. Bypass sends data to a third-party audience outside the operator's control loop. owner bypasses high by default under the role-tower model; trusted and member do not. The owner-in-public-channel defense lives in roles.owner.match[] discipline (default: TUI-only).
| Guard | Per-guard permission | Blocks |
|---|---|---|
outboundSecret | security.bypass.outboundSecret | outbound channel message containing a secret-shaped string |
systemPromptLeak | security.bypass.systemPromptLeak | outbound message echoing the system prompt |
gitRemoteTainted | security.bypass.gitRemoteTainted | push after a same-session git remote set-url |
Security guards — medium tier
Silent-attack severity. Bypass produces attacker-favorable state in operator-reviewable surface without immediate audience-leak. owner and trusted bypass by default; member does not.
| Guard | Per-guard permission | Blocks |
|---|---|---|
secretExfilBash | security.bypass.secretExfilBash | bash env, bash printenv, etc. |
secretExfilRead | security.bypass.secretExfilRead | read .env, read secrets.json, etc. |
ssrf | security.bypass.ssrf | curl http://169.254.169.254/... and other IMDS / SSRF targets |
sessionSearchSecrets | security.bypass.sessionSearchSecrets | session_search query returning secret-shaped hits |
gitExfil | security.bypass.gitExfil | git push to an unfamiliar remote (a clean operator-configured remote is not a leak; the retarget-and-push breach is gated by gitRemoteTainted at high) |
rolePromotion | security.bypass.rolePromotion | write/edit to typeclaw.json that widens role privileges or adds a privileged role |
cronPromotion | security.bypass.cronPromotion | write/edit to cron.json adding new jobs or changing scheduledByRole |
Security guards — low tier
Reserved tier. No inhabitants today. Exists so member's bypass.low grant has a forward-compatible home when a future guard ships at low. owner, trusted, and member all carry bypass.low by default.
Runtime role grants
The agent ships a grant_role tool so an operator can edit typeclaw.json#roles by conversation instead of hand-editing the file. It does one of two things per call:
- Match grant — assigns an author/scope to a role, e.g.
grant member the match rule slack:T0123 author:U_X. Takes effect immediately (the live permission table is hot-reloaded) and the change is committed totypeclaw.json. - Permission grant — adds a capability to a role, e.g.
grant guest channel.respond. Written totypeclaw.jsonbut restart-required: it lands on disk and takes effect on the nexttypeclaw restart.
Gates, all enforced at the tool boundary:
- Origin — callable only from the TUI or a 1:1 DM. A group/open channel turn is refused, because it mixes in other participants' messages (the confused-deputy / prompt-injection surface that could trick a trusted turn into rewriting the access-control table).
- Caller role — the caller must resolve to
ownerortrusted.memberandguestcannot grant. - Tier ceiling — a caller cannot grant a role above its own. A
trustedcaller can granttrusted/member/guest; onlyownercan grantowner. - Grant only what you hold — for permission grants, the caller can only confer a capability it itself possesses.
- No
security.bypass.*— bypass permissions disable guards rather than enable a feature;grant_rolerefuses them. They remain a deliberate hand-edit gated by therolePromotionguard.
This is the conversational counterpart to the CLI typeclaw role claim flow (/reference/cli#roles): role claim pairs the current author via a one-time code, while grant_role lets an already-trusted operator grant any author or capability mid-conversation.
Acceptance logic
tool.before accepts EITHER the tier permission (security.bypass.low|medium|high) OR the per-guard permission. The two axes work forever; adding a per-guard permission to a role doesn't remove the role's tier-wide grants.
The bundled security plugin reads each guard's exported GUARD_*_SEVERITY constant at boot. Adding a new guard without an exported severity is a TypeScript error, not a silent permissive fallback.