Skip to content

Audit Log JSON Schema

This page is the field-by-field reference for the JSON objects produced by runok audit --json (one object per line, JSONL). Use it to write jq queries against audit logs without reading the runok source.

The output contains two record types, distinguished by the kind field:

  • Decision entries (Top-Level Object) have no kind field. One is written per evaluation.
  • Ask resolution records (Ask Resolution Record) have kind: "ask_resolution". One is written when the user approves an ask decision in the agent’s permission dialog (requires the opt-in PostToolUse hook).

jq queries that only want decision entries can filter with select(.kind == null).

Every decision-entry line of runok audit --json output is one AuditEntry object with the fields listed below.

Example entry:

{
"timestamp": "2026-03-13T19:31:00.090565+00:00",
"command": "git push -f origin main",
"action": {
"type": "deny",
"detail": {
"message": "force push is forbidden",
"fix_suggestion": "git push origin main"
}
},
"sandbox_preset": null,
"default_action": "ask",
"metadata": {
"endpoint_type": "hook",
"session_id": "abc-123",
"cwd": "/home/user/project",
"tool_name": "Bash",
"hook_event_name": "PreToolUse",
"tool_use_id": "toolu_01AbCdEfGh"
},
"command_evaluations": [
{
"command": "git push -f origin main",
"action": {
"type": "deny",
"detail": {
"message": "force push is forbidden",
"fix_suggestion": "git push origin main"
}
},
"matched_rules": [
{
"action_kind": "deny",
"pattern": "git push -f|--force *",
"matched_tokens": ["origin", "main"]
}
],
"eval_type": "primary",
"argv": ["git", "push", "-f", "origin", "main"]
}
]
}

RFC 3339 timestamp in UTC, with sub-second precision and a +00:00 offset (e.g. 2026-03-13T19:31:00.090565+00:00), recording when the evaluation was performed. Note that the offset is written as +00:00, not Zjq literal-string comparisons (select(.timestamp >= "...")) need to use the same form.

Type: str
Always present: Yes

The input command string exactly as runok received it, before any shell parsing or compound-command splitting. For compound input this is the whole expression (a && b); for single input this is the same string as command_evaluations[0].command.

Type: str
Always present: Yes

Final evaluation result for the input as a whole. For compound input, this is the aggregated decision across all branches (the strictest result wins: any deny makes the whole input deny, etc.). See Action Object for the shape.

Type: Action
Always present: Yes

Name of the sandbox preset that was applied to this evaluation. null when no sandbox was applied or when multiple presets were merged (compound input where different branches matched different presets — the merged policy has no single canonical preset name). The preset name, when present, corresponds to a key under definitions.sandbox. See Sandbox merging for compound commands.

Type: str | null
Always present: Yes (may be null)

The configured defaults.action value at the time of evaluation. null when no default was configured. See defaults.action for the possible values.

Type: "allow" | "ask" | "deny" | null
Always present: Yes (may be null)

Session and context information about the invocation. See Metadata Object.

Type: Metadata
Always present: Yes

Per-branch evaluation records, in source order. One entry per shell command extracted from command:

  • A non-compound input (e.g. git status) produces exactly one entry with eval_type: "primary".
  • A compound or pipelined input (e.g. a && b, a || b, a ; b, a | b) produces one entry per branch, all with eval_type: "compound".
  • An input with no runnable command (comment-only, parse error) produces an empty array.

Each entry carries the rule-evaluation result (action, matched_rules) and the shell-level parse result (env, argv, redirects, pipe) side by side, so audit consumers can filter on the actual binary in one jq line:

Terminal window
runok audit --json | jq 'select(.command_evaluations[].argv[0] == "helmfile")'

See CommandEvaluation Object for the shape of each entry.

Type: list[CommandEvaluation]
Always present: Yes (may be empty)

Whether the ask was approved in the agent’s permission dialog, joined the same way as the ask ✓ marker in text mode (see Ask Resolution Record). Not part of the on-disk audit-log schema — computed only for this command’s output.

Type: bool
Present when: action.type is "ask". Omitted for every other entry.

The entry’s command, re-evaluated against the config currently in effect (as opposed to action / command_evaluations, which are a snapshot from when the entry was decided). Not part of the on-disk audit-log schema — computed only for runok audit --recheck output. See Recheck Object.

Type: Recheck
Present when: the --recheck flag was passed. Omitted otherwise.

Represents an evaluation result. The type field is a discriminator; detail is omitted for allow, and present (with type-specific keys) for deny and ask.

// allow
{ "type": "allow" }
// deny
{
"type": "deny",
"detail": {
"message": "force push is forbidden",
"fix_suggestion": "git push origin main"
}
}
// ask
{ "type": "ask", "detail": { "message": "are you sure?" } }

When no rule matches, the configured default_action is applied directly: type is "allow", "deny", or "ask" accordingly. There is no separate "default" discriminator in the audit-log JSON.

The kind of action.

Type: "allow" | "deny" | "ask"
Always present: Yes

ValueMeaning
allowThe command is permitted.
denyThe command is rejected.
askThe command requires user confirmation.

Optional message attached to the rule. For deny actions this is the rule’s message (see Denial Feedback); for ask actions this is the prompt shown to the user.

Type: str | null
Present when: type is deny or ask. The value may be null when the rule did not set message.

Optional fix-suggestion attached to a deny rule. See Denial Feedback.

Type: str | null
Present when: type is deny. The value may be null when the rule did not set fix_suggestion.

{
"endpoint_type": "hook",
"session_id": "abc-123",
"cwd": "/home/user/project",
"tool_name": "Bash",
"hook_event_name": "PreToolUse",
"tool_use_id": "toolu_01AbCdEfGh"
}

Which runok subcommand recorded this entry. Audit consumers can use this to distinguish hook invocations from explicit runok exec runs.

Type: "exec" | "hook"
Always present: Yes

ValueSource
execThe user invoked runok exec directly.
hookAn AI coding agent’s tool-use hook (e.g. Claude Code PreToolUse) invoked runok.

runok check is a dry-run evaluator and does not write audit log entries, so it never appears here.

Session identifier supplied by the calling environment, when available. For hook invocations from Claude Code, this is the Claude Code session ID. null when no session ID was provided.

Type: str | null
Always present: Yes (may be null)

Working directory at the time of evaluation. null when the working directory could not be determined.

Type: str | null
Always present: Yes (may be null)

Hook-specific: name of the tool the agent was about to run (e.g. Bash, Read). null when endpoint_type is not hook.

Type: str | null
Always present: Yes (may be null)

Hook-specific: name of the hook event (e.g. PreToolUse). null when endpoint_type is not hook.

Type: str | null
Always present: Yes (may be null)

Hook-specific: ID of the tool call this evaluation belongs to, as supplied by the agent. The agent sends the same tool_use_id to the PreToolUse and PostToolUse hooks of one tool call, so this is the correlation key between an ask decision entry and its Ask Resolution Record. null when endpoint_type is not hook, and in entries written before this field existed.

Type: str | null
Always present: Yes (may be null)

Written when the user approves an ask decision in the agent’s permission dialog. Recording these requires the opt-in PostToolUse hook (see Claude Code Integration); without it, no ask_resolution records appear.

Denials cannot be observed (see Claude Code Integration for why), so an ask entry without a resolution record means “denied or not yet decided”, not “denied”.

{
"kind": "ask_resolution",
"timestamp": "2026-03-13T19:32:10.512345+00:00",
"outcome": "approved",
"tool_use_id": "toolu_01AbCdEfGh",
"session_id": "abc-123",
"cwd": "/home/user/project",
"command": "terraform apply",
"executed_command": "runok exec --sandbox restricted -- 'terraform apply'"
}

Record type discriminator. Always the literal "ask_resolution". Decision entries have no kind field.

Type: "ask_resolution"
Always present: Yes

RFC 3339 timestamp in UTC of the approval (when the PostToolUse hook fired), in the same format as the decision-entry timestamp.

Type: str
Always present: Yes

How the ask was resolved. Currently always "approved".

Type: "approved"
Always present: Yes

Tool use ID of the approved tool call, matching metadata.tool_use_id of the correlated ask decision entry. null when neither the hook input nor the correlated entry carried a tool_use_id; correlation then fell back to session + command matching.

Type: str | null
Always present: Yes (may be null)

Session ID from the PostToolUse hook input.

Type: str | null
Always present: Yes (may be null)

Working directory from the PostToolUse hook input.

Type: str | null
Always present: Yes (may be null)

The original command, copied from the correlated ask decision entry. The record is self-contained: aggregating approvals by command needs no join back to decision entries.

Type: str
Always present: Yes

The command the agent actually executed (tool_input.command of the PostToolUse input). Differs from command when the PreToolUse response rewrote the command via updatedInput — with runok that happens for sandbox wrapping, producing the runok exec --sandbox <preset> -- '<command>' form.

Type: str
Always present: Yes

One entry per shell command extracted from the input. Higher-level shaping (resolving binary vs subcommand, normalising mise shims, classifying -n as boolean vs value-taking) is intentionally not done here because those rules differ per CLI and belong to the audit consumer.

{
"command": "FOO=x echo hi > /tmp/log",
"action": { "type": "allow" },
"eval_type": "compound",
"env": [{ "name": "FOO", "value": "x" }],
"argv": ["echo", "hi"],
"redirects": [
{
"redirect_type": "output",
"operator": ">",
"target": "/tmp/log",
"descriptor": null
}
],
"pipe": { "stdin": false, "stdout": true }
}

When variable resolution rewrote the branch, original_command carries the verbatim source:

{
"command": "git push --force",
"original_command": "git push $F",
"action": {
"type": "deny",
"detail": { "message": null, "fix_suggestion": null }
},
"matched_rules": [
{
"action_kind": "deny",
"pattern": "git push --force*",
"matched_tokens": []
}
],
"eval_type": "primary",
"argv": ["git", "push", "--force"]
}

The branch command as runok extracted it, with redirects stripped but the inline env prefix kept. This is the text rule evaluation actually used, so when variable resolution rewrote a $X / ${X} reference to its statically known value, this is the expanded text — see original_command for the verbatim source. For eval_type: "primary" entries where nothing was expanded, this is identical to the top-level command; when a rewrite did happen, it’s original_command (not command) that matches the top-level value.

Type: str
Always present: Yes

The verbatim source text of this branch, before variable resolution rewrote command to its expanded form.

Type: str
Omitted when nothing was resolved (command already is the original text).

Rule-evaluation result for this branch. See Action Object.

Type: Action
Always present: Yes

Rules that matched for this branch, in match order. See RuleMatch Object.

Type: list[RuleMatch]
Omitted when empty.

How this branch was extracted from the input.

Type: "primary" | "compound"
Always present: Yes

ValueMeaning
primaryNon-compound input. The single entry covers the whole input.
compoundOne branch of a compound or pipelined input (a && b, a || b, a ; b, a | b, etc.).

Inline KEY=VALUE env prefix attached to this branch. See EnvVar Object.

Type: list[EnvVar]
Omitted when empty.

Command name plus arguments, with shell quoting resolved. argv[0] is the binary as written. Empty (and therefore omitted) when shell parsing could not produce an argv (AST leaf-text fallback path).

Type: list[str]
Omitted when empty.

Redirect operators attached to this branch. See Redirect Object.

Type: list[Redirect]
Omitted when empty.

Pipeline position of this branch. See Pipe Object.

Type: Pipe
Omitted when both stdin and stdout are false (i.e. the branch is not part of a pipeline).

{
"action_kind": "deny",
"pattern": "git push -f|--force *",
"matched_tokens": ["origin", "main"]
}

The kind of rule that matched.

Type: "allow" | "ask" | "deny"
Always present: Yes

The rule pattern string as written in runok.yml. See Pattern Syntax.

Type: str
Always present: Yes

Tokens the wildcard portion of the pattern captured. For example, the pattern git push -f|--force * matched against git push -f origin main yields ["origin", "main"]. Empty for patterns with no wildcards.

Type: list[str]
Always present: Yes (may be empty)

{ "name": "FOO", "value": "x" }

Variable name.

Type: str
Always present: Yes

Variable value with shell quotes resolved. null for the bare KEY= cmd form (which clears the variable in the child process’s environment).

Type: str | null
Always present: Yes (may be null)

Captures redirect operators (> file, 2>&1, <<< here-strings, << here-docs, etc.). The here-doc delimiter and body are not captured — only the operator itself.

{
"redirect_type": "output",
"operator": ">",
"target": "/tmp/log",
"descriptor": null
}

Redirect category.

Type: "input" | "output" | "dup"
Always present: Yes

ValueExamples
input<file, <<EOF, <<-EOF, <<<"string"
output>file, >>file, &>file
dup2>&1, >&2 (file-descriptor duplication)

The redirect operator text.

Type: str
Always present: Yes

Redirect target. A filename for file redirects, an fd reference like &1 for dup redirects, or an empty string for << / <<- here-docs (the delimiter is not captured).

Type: str
Always present: Yes (may be empty)

Explicit file descriptor when the redirect specifies one (e.g. 2 in 2>file). null when the redirect uses the default fd (stdin for input, stdout for output).

Type: int | null
Always present: Yes (may be null)

{ "stdin": true, "stdout": false }

true when this branch’s stdin comes from a preceding pipe (i.e. there is a ... | this upstream).

Type: bool
Always present: Yes

true when this branch’s stdout feeds a following pipe (i.e. there is a this | ... downstream).

Type: bool
Always present: Yes

Present only in runok audit --recheck output, as the top-level recheck field. Serialises as one of two shapes, distinguishable by which keys are present — there is no type/status discriminator field.

On success:

{
"action": { "type": "allow" },
"command_evaluations": [
{
"command": "terraform apply",
"action": { "type": "allow" },
"matched_rules": [
{
"action_kind": "allow",
"pattern": "terraform *",
"matched_tokens": ["apply"]
}
]
}
]
}

On failure (the entry has no recorded metadata.cwd, its config failed to load, or re-evaluation itself errored — see --recheck):

{ "error": "failed to load config: ..." }

The current evaluation result for command as a whole, in the same Action Object shape as the top-level action field — consumers can reuse the same parsing code for both.

Type: Action
Present when: re-evaluation succeeded.

Per-branch re-evaluation results, in the same order as command_evaluations. See RecheckCommandEvaluation Object.

Type: list[RecheckCommandEvaluation]
Present when: re-evaluation succeeded.

Why the entry could not be re-evaluated.

Type: str
Present when: re-evaluation failed.

{
"command": "terraform apply",
"action": { "type": "allow" },
"matched_rules": [
{
"action_kind": "allow",
"pattern": "terraform *",
"matched_tokens": ["apply"]
}
]
}

The branch command, in the same form as CommandEvaluation.command.

Type: str
Always present: Yes

Current re-evaluation result for this branch. See Action Object.

Type: Action
Always present: Yes

Rules that currently match this branch, in the same RuleMatch Object shape as CommandEvaluation.matched_rules. A branch with action.type: "ask" and this omitted (empty) is resolved for that branch purely via defaults.action fallback, not an explicit ask rule. The ask-def NOW-column label (and the --recheck example jq query) combines this with action.type across all branches, rather than reading a single branch’s matched_rules in isolation — a branch matched by an allow rule still has non-empty matched_rules, so a compound command escalated to ask by a sandbox contradiction (all branches matched an allow rule, none matched ask) is correctly excluded from ask-def.

Type: list[RuleMatch]
Omitted when empty.