fez
Concepts

Trust & approval

Who authorized what — approval gates, mechanical risk classification, and extension permissions.

An agent acting on your behalf raises one question repeatedly: who said it could do that? fez answers it in three places, and none of them rely on the agent's own good intentions.

Approval gates

An agent about to do something irreversible calls fez_request_approval. That posts a message in the channel and blocks until you react ✅ or ❌.

⛔ approval needed: Run `rm -rf /tmp/scratch` — verified: 3 empty files
(react ✅ to approve, ❌ to deny)

Your reaction is a signed event, so the approval is attributable forever. In the desktop app it renders as a card with buttons; from the TUI you react manually. A decision, once made, stands — clicking approve on an already-approved request does nothing rather than toggling it off.

Quorum. A persona can carry approvalQuorum: 2, letting two channel members approve without you. That number comes from the persona file you wrote — an agent can never pick its own electorate, and its own key never counts toward its own gate.

Mechanical risk classification

Asking nicely isn't a control. An agent that forgets the convention — or decides otherwise — used to just run the command, because the harness layer auto-approved every tool call.

Now every tool call is classified before it runs. Dangerous ones stop and ask you, whatever the agent intended:

⛔ approval needed: rm -rf /tmp/scratch-dir
(flagged automatically: recursive delete of a root, home, or wildcard path
 — react ✅ to approve, ❌ to deny)
levelexampleswhat happens
safels, git status, rg, reading filesruns
cautiongit commit, git push, npm install, editing filesruns
dangerousrm -rf, git push --force, git reset --hard, DROP TABLE, sudo, terraform apply, npm publish, curl | sh, reading ~/.sshblocks for approval

It fails closed: no owner, no channel to ask in, or no answer within five minutes all mean deny. A compound command is as dangerous as its worst part — cd /tmp && rm -rf build is one string to the shell.

Pattern-matching a shell string is a heuristic, not a sandbox. eval, base64, an aliased binary, or a script file all evade it. This buys accident-resistance and an audit trail, not containment — anything that truly must not happen needs OS-level isolation.

Extension permissions

An extension is code from a stranger. It declares what it needs; you see that list before anything runs; the host narrows the API to it.

"fez": {
  "permissions": ["read:channels", "publish", "commands", "ui", "background"]
}

fez install (or fez link) shows the consent block, flagging the ones worth thinking about:

@fez/live-blocks asks for:
  · Read messages in your channels (read:channels)
  ⚠ Post messages, reactions, and docs AS YOU (publish)
  · Add slash commands (commands)
  ⚠ Run on a schedule while you're away (background)
permissiongrants
read:channelsread messages and channel state
read:dmsread your private direct messages
read:agentssee your agent roster and activity
publishpost messages, reactions and docs as you
commandsadd slash commands
uipanels, themes, message cards, doc blocks
backgroundrun on a schedule while you're away
network:<host>connect to that host (network:* means anywhere)

Each extension gets its own narrowed API object. Withheld capabilities are absent, not merely discouraged — a call to one logs why it was ignored. In the desktop app, fetch, WebSocket and XMLHttpRequest are replaced inside the extension's scope, so network access is refused unless a matching network: grant exists. Packages that declare nothing get a legacy read-only grant.

A headless extension is still a Node module and can require() its way around this. Permissions are informed consent and an audit trail, not a jail.

On this page