Skip to Content

Permissions

cmdop permissions is the CRUD surface for the permission gate that sits between an incoming remote-agent tool call and your daemon.

Permissions apply only to incoming remote-agent calls. Local cmdop chat, the desktop, and the SDK bypass the gate intentionally — the operator is driving their own agent, an extra confirmation modal is just friction.

What the gate guards

When vps-bmw’s agent calls ask_agent vps-audi 'do X', the gate on vps-audi decides whether vps-audi will let the request execute:

  • Allow — run silently, no prompt.
  • Deny — refuse with a structured error.
  • Ask — push a modal to the operator’s TUI / desktop, 60s timeout, default deny.

Decision order

  1. Floor — non-bypassable safety net (rm -rf /, /etc/shadow writes, fork bombs).
  2. Best-matching rule — session rules before global; deny > ask > allow.
  3. Mode default — if nothing matched.

Modes

cmdop permissions mode # show cmdop permissions mode default # most tools allow, dangerous ask cmdop permissions mode strict # most tools ask, dangerous deny cmdop permissions mode bypass # everything allow (test machines only)
ModeDefault behaviourUse for
defaultMost tools allow, dangerous askDaily ops on trusted hosts
strictMost tools ask, dangerous denyProduction, shared machines
bypassEverything allow (floor still applies)Throwaway test VMs

Rule grammar

tool(argGlob) # execute_command(git *) dispatcher(operation:argGlob) # connect(exec:prod-*)

Tool → arg matcher table:

ToolArg matched
execute_commandcommand
write_file, read_filepath
connect, ssh_session, ask_agenthostname

Globs use shell-style wildcards (*, ?, [abc]). Leading ! negates.

CLI verbs

cmdop permissions list # all rules cmdop permissions list --mode session # session-only cmdop permissions allow 'execute_command(git *)' cmdop permissions allow 'execute_command(git *)' --reason "trusted repo" cmdop permissions deny 'execute_command(rm *)' cmdop permissions deny 'write_file(.env*)' cmdop permissions ask 'write_file' cmdop permissions revoke 'execute_command(git *)' cmdop permissions audit # tail audit log cmdop permissions audit -n 100 --since 1h

Where rules live

PathPurpose
~/.cmdop/permissions.yamlWorkspace + global rules (mode 0600)
~/.cmdop/logs/audit.logJSON-line audit trail (lumberjack-rotated)

Rule file shape:

version: 1 mode: default allow: - execute_command(git *) - read_file deny: - execute_command(rm *) - write_file(.env*) ask: - write_file reasons: "execute_command(git *)": "trusted repo" created_at: "execute_command(git *)": "2026-04-23T10:00:00Z"

Audit log

JSON lines. Fields: ts, decision, tool, operation, target, mode, source, rule_id, reason, input_digest, ask_ms, hostname, user.

cmdop permissions audit cmdop permissions audit --tool execute_command cmdop permissions audit --decision deny --since 24h cmdop permissions audit --json | jq -r '. | select(.decision == "deny")'

Self-to-self bypass

When vps-audi’s agent calls ask_agent vps-audi, the gate is skipped — the daemon recognises self-identity via CallerHostname server-side. Operator-on-self overhead would be useless friction.

Self-to-self bypass means a compromised agent on vps-audi can call its own tools freely. This is by design — the threat model assumes the daemon’s identity is the trust boundary. Lateral movement to other machines hits their gate.

Feature flag

The whole gate can be disabled via permissions.enabled: false in ~/.cmdop/config.yaml — but the floor still applies. Default: enabled.

cmdop config set permissions.enabled false cmdop agent restart

Examples

Lock production tightly:

cmdop permissions mode strict cmdop permissions deny 'execute_command(rm *)' cmdop permissions deny 'write_file(/etc/*)' cmdop permissions deny 'write_file(.env*)' cmdop permissions allow 'read_file' cmdop permissions allow 'execute_command(git pull *)'

Open a CI runner:

cmdop permissions mode bypass # floor still blocks rm -rf / and friends
Last updated on