Skip to Content

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:

  1. Floor (non-bypassable) — see below.
  2. Best rule match. Session rules win over global; within scope deny > ask > allow.
  3. 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 familyArg key
execute_commandcommand
write_file / read_file / open_filepath
connect / ssh_session / ask_agenthostname (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:

ModeBehaviour when no rule matches
defaultAsk the user via the modal. Safe baseline for laptops.
strictDeny. Use on prod hosts that receive cross-machine asks.
bypassAllow (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 50

Hands-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.

TAGS: permissions, gate, floor, rule, audit, modes DEPENDS_ON: [agent-communication, daemon, tools]

Last updated on