Debugging denied calls
A remote agent says “I cannot do that” and you need to know why. This guide walks through the four common causes and how to fix each.
Step 1 — capture the deny
The remote agent’s response includes a structured deny:
{
"error": "permission_denied",
"tool": "execute_command",
"target": "rm -rf /tmp/build",
"decision": "deny",
"source": "rule",
"rule_id": "ghi789",
"reason": "no destructive removes"
}If you do not see this shape, your caller may not be propagating gate errors — check the SDK / chat client.
Step 2 — confirm on the receiver
SSH into the receiver (or use cmdop connect from somewhere else) and tail audit:
cmdop permissions audit --tail 20Find the matching JSON line. Note source — it determines the fix.
Step 3 — fix by source
source: floor
The floor protected the path / command. You cannot override the floor. Examples:
- Writing
.env,.gitconfig,.bashrc,.zshrc,.profile,.ripgreprc,.mcp.json,.claude.json. - Touching
.git/,.ssh/,.cmdop/. - Reading or writing under
/etc,/System,/private/etc,~/Library/Keychains,~/.config/cmdop. - Bash signatures:
rm -rf /, fork bombs,| sh,| bash,> /dev/sda.
Fix: refactor the agent’s call. Read from a safe location, write to a project subdirectory, use a less destructive command.
source: rule
A user-authored rule fired. Inspect:
cmdop permissions list | grep <rule_id>Decide:
-
Rule was correct? Tell the agent to take a different path.
-
Rule was wrong? Revoke or rewrite:
cmdop permissions revoke <rule_id> cmdop permissions allow 'execute_command(rm /tmp/*)'Be specific — broad allows undo the safety of the gate.
source: mode
No rule matched, the mode said deny. Two possibilities:
-
Mode is
strict— add anallowrule for the legitimate case:cmdop permissions allow 'execute_command(systemctl status *)' -
Mode is
defaultand asks timed out — the daemon couldn’t reach a UI subscriber. Either start the desktop app / TUI on the receiver, or switch tostrictwith an explicit allowlist for headless servers.
source: ask_timeout
Same as above — no UI to decide. See reason: "no UI wired". Fix: attach a UI or convert the rule to allow / deny so it does not require a human.
Step 4 — let the agent help
If the agent itself notices the deny pattern, it can call permissions_helper(operation=suggest_rule, ...) to route a proposed rule through the modal. The proposal lands in your TUI / desktop pop-up; you accept or reject.
Status codes you may see come back:
accepted,denied— user decided.validated_no_ui— no subscriber; the agent gets a CLI command to suggest you run.rejected_floor— the floor blocked the proposal.rejected_action— onlyallow/askare accepted from the agent (nodeny).rejected_syntax— pattern did not parse.
Step 5 — verify the fix
Re-run the call and re-check audit. The same tool + target should now show decision: allow (or ask_allowed if you added an ask rule and decided yes in the modal).
Common gotchas
- Quoting —
cmdop permissions allow execute_command(git *)will be eaten by your shell. Always quote:cmdop permissions allow 'execute_command(git *)'. - Hostname casing —
prod-1vsProd-1. Hostnames are case-insensitive on macOS, sensitive on Linux. Match what the audit log shows. - Glob depth —
~/projects/*matches~/projects/a.txtbut not~/projects/sub/a.txt. Use**for any depth. - Self-to-self bypass — same OAuth user → same machine = no gate fires. Test denies against a different user / different machine.
Self-to-self calls (same OAuth user) skip the permission gate by design. If you want to hard-gate everything, run the receiver under a different account.