Skip to Content

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 20

Find 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 an allow rule for the legitimate case:

    cmdop permissions allow 'execute_command(systemctl status *)'
  • Mode is default and asks timed out — the daemon couldn’t reach a UI subscriber. Either start the desktop app / TUI on the receiver, or switch to strict with 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 — only allow / ask are accepted from the agent (no deny).
  • 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

  • Quotingcmdop permissions allow execute_command(git *) will be eaten by your shell. Always quote: cmdop permissions allow 'execute_command(git *)'.
  • Hostname casingprod-1 vs Prod-1. Hostnames are case-insensitive on macOS, sensitive on Linux. Match what the audit log shows.
  • Glob depth~/projects/* matches ~/projects/a.txt but 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.

Last updated on