Skip to Content

Permission audit log

Every gate decision lands in audit.log on the receiver machine. This guide is how to read it.

The permission audit log is per-machine. The cabinet’s audit log is per-request server-side. They cover different layers and both are useful.

Where it lives

File: <LogDir>/audit.log. Resolved per platform (utils.LogDir()):

  • macOS: ~/Library/Logs/cmdop/audit.log
  • Linux: ~/.local/state/cmdop/audit.log (respects XDG_STATE_HOME)
  • Windows: %LOCALAPPDATA%\cmdop\Logs\audit.log

JSON-line format, lumberjack-rotated (10 MB × 5 files = 50 MB max).

Reading it

The CLI tail is the easiest:

cmdop permissions audit --tail 50 cmdop permissions audit --since 1h cmdop permissions audit --tool execute_command cmdop permissions audit --decision deny

Fields

Every entry includes:

FieldMeaning
tsISO timestamp.
decisionallow, deny, ask_allowed, ask_denied, ask_timeout.
toolThe tool name, e.g. execute_command.
operationDispatcher operation, e.g. exec for connect(exec:...).
targetArgument the matcher ran against (command / path / hostname).
modeActive mode at decision time.
sourcefloor, rule, or mode.
rule_idSha256 of the matched rule (empty if source != rule).
reasonReason from rule, floor message, or “no UI wired”.
input_digestSha256 of the full tool input — useful for correlating with logs.
ask_msMilliseconds spent waiting on a UI decision (only for ask_*).
hostnameCaller hostname, populated from gRPC metadata.
userCaller OAuth user, if available.

Sample entries

{"ts":"2026-04-25T12:34:56Z","decision":"allow","tool":"execute_command","target":"git status","mode":"default","source":"rule","rule_id":"abc123","reason":"developer convenience"} {"ts":"2026-04-25T12:35:11Z","decision":"deny","tool":"write_file","target":"/Users/me/.env","mode":"default","source":"floor","reason":"protected file suffix"} {"ts":"2026-04-25T12:35:42Z","decision":"ask_allowed","tool":"write_file","target":"/Users/me/projects/notes.md","mode":"default","source":"rule","rule_id":"def456","ask_ms":1834} {"ts":"2026-04-25T12:36:01Z","decision":"ask_timeout","tool":"execute_command","target":"npm test","mode":"default","source":"rule","rule_id":"def456","reason":"no UI wired","ask_ms":60000}

Common patterns

“What did the agent want last hour?”

cmdop permissions audit --since 1h | jq -s 'group_by(.tool) | map({tool: .[0].tool, count: length}) | sort_by(-.count)'

“Show me only blocked calls.”

cmdop permissions audit --decision deny --tail 100

“Find what triggered a specific rule.”

cmdop permissions audit --tail 1000 | jq 'select(.rule_id == "abc123")'

Retention

10 MB × 5 files locally. For longer retention, ship the file to a SIEM via your log shipper of choice (Vector, Fluent Bit, Datadog Agent). The path is stable across daemon restarts.

The local audit log is local. If the machine is wiped or the disk fails, the audit history is gone. Ship to a centralized store for compliance.

Where the writer lives

internal/agent/permissions/audit.go writes JSON lines via lumberjack. The build-up function Build(BuildConfig) in internal/agent/permissions/build.go wires the audit logger to the gate at daemon start.

Last updated on