Permissions
The permission gate inspects every tool call from a remote agent against
permissions.yaml and returns Allow, Deny, or Ask. Local chat — your own cmdop chat,
TUI, and SDK calls on the same OAuth identity — bypasses the gate by design.
What the gate does
The gate is the receiver’s defence in depth. When agent A on machine A1 calls ask_agent
on machine A2, A2’s daemon wraps every inbound tool call in permissions.Gate.Check. The
gate consults the floor (non-bypassable), the rule set, and the active mode, then returns
a typed decision and writes an audit line.
See internal/agent/permissions/CLAUDE.md for the authoritative description and report 04
§5 for the chat-side wiring.
Decision order
The hard order:
- Floor (non-bypassable) — see below.
- Best rule match. Session rules win over global; within scope
deny>ask>allow. - Mode default when no rule matches:
default→ ask the user,strict→ deny,bypass→ allow (subject to floor).
Rule grammar
Rules use a gitignore-style glob over the tool’s most relevant argument.
execute_command(git *)
execute_command(rm *)
write_file(.env*)
write_file(~/projects/**)
read_file(/var/log/**)
connect(exec:prod-*)
ssh_session(open:internal-*)The argument key per tool comes from matcher.go::argKeyForTool:
| Tool family | Arg key |
|---|---|
execute_command | command |
write_file / read_file / open_file | path |
connect / ssh_session / ask_agent | hostname (with optional operation: prefix) |
Dispatcher tools (connect, ssh_session) accept the operation:argGlob form so you can
restrict by sub-action.
Modes
cmdop permissions mode <name> sets the default for unmatched tool calls:
| Mode | Behaviour when no rule matches |
|---|---|
default | Ask the user via the modal. Safe baseline for laptops. |
strict | Deny. Use on prod hosts that receive cross-machine asks. |
bypass | Allow (floor still applies). Only on isolated VMs you fully trust. |
The floor — non-bypassable
The floor is hardcoded protection that cannot be loosened by any rule or mode:
- Files:
.env,.gitconfig,.bashrc,.zshrc,.profile,.ripgreprc,.mcp.json,.claude.json. - Directories:
.git,.ssh,.cmdop. - Absolute prefixes:
/etc,/System,/private/etc,~/Library/Keychains,~/.config/cmdop. - Bash signatures:
rm -rf /, fork bombs (:(){:|:&};:),| sh,| bash,> /dev/sda, and similar.
Source: internal/agent/permissions/floor.go.
The floor is hardcoded. There is no flag to disable it. Even mode: bypass cannot reach
past it.
Storage
Rules and mode live at ~/<config>/permissions.yaml, mode 0600, written atomically
(temp file + rename). Schema:
version: 1
mode: default # default | strict | bypass
allow:
- execute_command(git *)
- read_file(/var/log/**)
deny:
- execute_command(rm *)
- write_file(.env*)
ask:
- write_file(~/projects/**)
reasons:
"execute_command(git *)": "trusted repo workflow"
created_at:
"execute_command(git *)": "2026-04-23T10:00:00Z"See internal/agent/permissions/store.go.
Ask flow — the modal
When a rule resolves to ask (or no rule matches in default mode), the gate calls
Gate.Resolve, which publishes over the permissionsbus Unix socket. TUI and Desktop
subscribers display a modal. First decision wins; no decision in 60 s → deny.
If no UI is attached, the gate denies with reason ask_timeout: no UI wired.
Audit log
Every decision is appended as one JSON line to ~/<log>/audit.log. Lumberjack rotates at
10 MB × 5.
{
"ts": "2026-04-27T14:55:12Z",
"decision": "deny",
"tool": "execute_command",
"operation": "",
"target": "rm -rf /tmp/build",
"mode": "default",
"source": "rule",
"rule_id": "deny:execute_command(rm *)",
"reason": "blocked by user rule",
"input_digest": "sha256:8c7f...",
"ask_ms": 0,
"hostname": "prod-1",
"user": "[email protected]"
}Agent-facing helper tool
The agent itself can introspect the policy via permissions_helper:
{
"operation": "explain",
"tool": "execute_command",
"args": { "command": "rm -rf node_modules" }
}Operations:
explain— why the gate would decide as it does for a hypothetical call.show_policy— current rules and mode (read-only).show_audit_context— last few audit lines for the same tool.suggest_rule— propose a rule to the user via the modal. Status codes:accepted,denied,validated_no_ui,rejected_floor,rejected_action,rejected_syntax.
The agent cannot write rules directly. See report 08 §5.
CLI
cmdop permissions exposes the gate to humans:
cmdop permissions list
cmdop permissions allow 'execute_command(git *)'
cmdop permissions deny 'execute_command(rm *)'
cmdop permissions ask 'write_file(~/projects/**)'
cmdop permissions revoke <rule_id>
cmdop permissions mode strict
cmdop permissions audit --tail 50Hands-on examples live in Agent Permissions and the CLI reference.
Callouts
The gate fires only for remote agent calls. Your own cmdop chat and SDK code on the
same OAuth identity always run unsupervised.
mode: bypass allows everything that the floor doesn’t block. Use only on isolated VMs
you fully trust.
Related
TAGS: permissions, gate, floor, rule, audit, modes DEPENDS_ON: [agent-communication, daemon, tools]