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
- Floor — non-bypassable safety net (
rm -rf /,/etc/shadowwrites, fork bombs). - Best-matching rule — session rules before global; deny > ask > allow.
- 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)| Mode | Default behaviour | Use for |
|---|---|---|
default | Most tools allow, dangerous ask | Daily ops on trusted hosts |
strict | Most tools ask, dangerous deny | Production, shared machines |
bypass | Everything allow (floor still applies) | Throwaway test VMs |
Rule grammar
tool(argGlob) # execute_command(git *)
dispatcher(operation:argGlob) # connect(exec:prod-*)Tool → arg matcher table:
| Tool | Arg matched |
|---|---|
execute_command | command |
write_file, read_file | path |
connect, ssh_session, ask_agent | hostname |
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 1hWhere rules live
| Path | Purpose |
|---|---|
~/.cmdop/permissions.yaml | Workspace + global rules (mode 0600) |
~/.cmdop/logs/audit.log | JSON-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 restartExamples
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