$ shellfirm

Log Format Reference

Complete specification of shellfirm's JSON-lines audit log format

shellfirm writes audit events as JSON-lines: one JSON object per line, appended to ~/.shellfirm/audit.log. This format is easy to parse, stream, and ingest into log aggregation systems.

Format

Each line is a valid JSON object:

{"event_id":"a1b2c3d4","timestamp":"2026-02-23T14:30:00Z","command":"git push --force origin main","matched_ids":["git:force_push"],"challenge_type":"Yes","outcome":"DENIED","context_labels":["branch=main"],"severity":"High","agent_name":"claude-code","agent_session_id":"sess-xyz-789","blast_radius_scope":"PROJECT","blast_radius_detail":"Overwrites remote branch history"}

Formatted for readability:

{
  "event_id": "a1b2c3d4",
  "timestamp": "2026-02-23T14:30:00Z",
  "command": "git push --force origin main",
  "matched_ids": ["git:force_push"],
  "challenge_type": "Yes",
  "outcome": "DENIED",
  "context_labels": ["branch=main"],
  "severity": "High",
  "agent_name": "claude-code",
  "agent_session_id": "sess-xyz-789",
  "blast_radius_scope": "PROJECT",
  "blast_radius_detail": "Overwrites remote branch history"
}

Field reference

Required fields (always present)

FieldTypeDescription
event_idstringUnique identifier for correlating pre/post challenge entries
timestampstringISO 8601 UTC timestamp (YYYY-MM-DDTHH:MM:SSZ)
commandstringThe full command string that was evaluated
matched_idsstring[]List of pattern IDs that matched (e.g., ["git:force_push"])
challenge_typestringChallenge type applied: Math, Enter, Yes, or Deny
outcomestringResult of the evaluation (see Outcomes below)
context_labelsstring[]Runtime context labels (e.g., ["branch=main", "ssh=true"])
severitystringHighest severity among matched patterns: Info, Low, Medium, High, Critical

Optional fields (present when applicable)

FieldTypeDescription
agent_namestringName of the AI agent that originated the command
agent_session_idstringSession ID of the agent connection
blast_radius_scopestringImpact scope: RESOURCE, PROJECT, or MACHINE
blast_radius_detailstringHuman-readable impact description

Outcomes

OutcomeDescription
ALLOWEDUser passed the challenge or the command was below threshold
DENIEDUser failed the challenge, command was on deny list, or agent auto-deny blocked it
SKIPPEDPattern matched but severity was below min_severity threshold
CANCELLEDWritten before the challenge prompt; indicates the evaluation started but no decision was recorded yet

Event correlation

Each command evaluation can produce two entries with the same event_id:

  1. A CANCELLED entry written before the challenge prompt begins
  2. An ALLOWED or DENIED entry written after the user responds

If only a CANCELLED entry exists (no matching ALLOWED/DENIED), the user hit Ctrl+C during the challenge.

For agent/MCP commands, only the final outcome entry is written (no CANCELLED pre-entry).

Context labels

Context labels are strings in key=value format:

LabelMeaning
ssh=trueCommand was run in an SSH session
root=trueCommand was run as root
branch=mainCurrent git branch is main
k8s=prod-us-east-1Active Kubernetes context
NODE_ENV=productionProduction environment variable detected

Example entries

User allows a risky command

{"event_id":"evt-001","timestamp":"2026-02-23T14:30:00Z","command":"rm -rf /tmp/old-data","matched_ids":["fs:recursively_delete"],"challenge_type":"Math","outcome":"ALLOWED","context_labels":[],"severity":"High"}

Command denied on production branch

{"event_id":"evt-002","timestamp":"2026-02-23T14:31:00Z","command":"git push --force origin main","matched_ids":["git:force_push"],"challenge_type":"Yes","outcome":"DENIED","context_labels":["branch=main"],"severity":"High"}

Agent command auto-denied

{"event_id":"evt-003","timestamp":"2026-02-23T14:32:00Z","command":"kubectl delete namespace production","matched_ids":["kubernetes:delete_namespace"],"challenge_type":"Deny","outcome":"DENIED","context_labels":["k8s=prod-us-east-1"],"severity":"Critical","agent_name":"claude-code","agent_session_id":"sess-abc-123","blast_radius_scope":"PROJECT","blast_radius_detail":"Deletes all resources in the namespace"}

Command skipped (below min_severity)

{"event_id":"evt-004","timestamp":"2026-02-23T14:33:00Z","command":"git stash drop","matched_ids":["git:stash_drop"],"challenge_type":"Math","outcome":"SKIPPED","context_labels":[],"severity":"Medium"}

Parsing examples

Python

import json

with open("~/.shellfirm/audit.log") as f:
    for line in f:
        event = json.loads(line.strip())
        if event["outcome"] == "DENIED":
            print(f"{event['timestamp']}: {event['command']}")

Shell (with jq)

# Parse each line
while IFS= read -r line; do
  echo "$line" | jq -r '[.timestamp, .outcome, .command] | @tsv'
done < ~/.shellfirm/audit.log

Node.js

const fs = require('fs');
const lines = fs.readFileSync('~/.shellfirm/audit.log', 'utf8').trim().split('\n');
const events = lines.map(line => JSON.parse(line));
const denied = events.filter(e => e.outcome === 'DENIED');
console.log(`${denied.length} commands were denied`);