You are the audit skill — read-only window onto the unified Lintel audit trail (v4.0 Phase 1).
What this skill does
Every Lintel audit record lands in <category>.jsonl via the unified audit_log writer in bin/_audit.sh. Since v5 the trail is split in two: repo events (cycle runs, jobs, brief-forge, granularity, …) land in <repo>/.claude/runtime/audit/<category>.jsonl; operator events (pack lifecycle, pack-resolver, migrations, usage-*) stay in ~/.lintel/audit/<category>.jsonl. This reader checks the repo dir first, then global. Each record is a single JSON line with the shape:
{"ts":"...","kind":"...","operator":"...","cycle_id":"...", ...extra k=v fields...}
This skill cats/greps those logs so an operator can answer "what happened, and when?" without hand-parsing JSONL. It never writes — pure read.
When to use
- "What's in the audit trail?" — no args, lists every category + record count
- "Show me the jobs audit" —
--category jobs - "Did any brief-forge bypass fire this week?" —
--kind brief_forge_bypassed --since 7 - After a meta-infra change — confirm the expected override/pack-resolver events landed
When NOT to use
- To WRITE audit records — that's
audit_loginbin/_audit.sh, called by the producing code - For in-flight job status — use
/li:status(reads_active.md, not the audit log) - For git history — this is Lintel's internal event trail, not VCS
Inputs
All optional:
--category <name>— restrict to one log file (e.g.jobs,pack-resolver,brief-forge,alias-resolution,hooks). Default: all categories.--kind <kind>— filter records by theirkindfield (e.g.job_begin,brief_forge_bypassed).--since <N>— only records from the last N days (ISO-8601 string compare onts).--limit <N>— cap output lines (default 50).
Workflow
LINTEL_HOME="${LINTEL_HOME:-$HOME/.lintel}"
REPO_ROOT="${LINTEL_REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null)}"
# v5 split: repo events in <repo>/.claude/runtime/audit/, operator events
# (pack-lifecycle, pack-resolver, migrations, usage-*) in ~/.lintel/audit/.
# Repo dir is checked first, then global. LINTEL_AUDIT_DIR overrides both.
if [ -n "${LINTEL_AUDIT_DIR:-}" ]; then
audit_dirs=("$LINTEL_AUDIT_DIR")
else
audit_dirs=()
[ -n "$REPO_ROOT" ] && [ -d "$REPO_ROOT/.claude/runtime/audit" ] && audit_dirs+=("$REPO_ROOT/.claude/runtime/audit")
[ -d "$LINTEL_HOME/audit" ] && audit_dirs+=("$LINTEL_HOME/audit")
fi
category=""
kind=""
since=""
limit=50
while [ $# -gt 0 ]; do
case "$1" in
--category) category="$2"; shift 2 ;;
--kind) kind="$2"; shift 2 ;;
--since) since="$2"; shift 2 ;;
--limit) limit="$2"; shift 2 ;;
*) echo "Usage: /li:audit [--category <c>] [--kind <k>] [--since <days>] [--limit <n>]"; exit 2 ;;
esac
done
if [ "${#audit_dirs[@]}" -eq 0 ]; then
echo "_No audit trail yet._ (Nothing has been audit-logged in this repo or on this machine.)"
exit 0
fi
# No filters → summary table of categories + counts (repo dir first, then global)
if [ -z "$category" ] && [ -z "$kind" ] && [ -z "$since" ]; then
echo "## Lintel audit categories"
found=0
for d in "${audit_dirs[@]}"; do
echo ""
echo "### $d"
for f in "$d"/*.jsonl; do
[ -f "$f" ] || continue
found=1
n=$(wc -l < "$f" | tr -d ' ')
printf -- '- %s: %s records\n' "$(basename "$f" .jsonl)" "$n"
done
done
[ "$found" -eq 0 ] && echo "_No audit records yet._"
echo ""
echo "Run with --category <name> to view records."
exit 0
fi
# Build the cut-off ISO timestamp for --since
since_iso=""
if [ -n "$since" ]; then
since_iso=$(date -u -d "$since days ago" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || \
date -u -v "-${since}d" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "")
fi
# Resolve which files to scan (repo dir first, then global)
files=()
for d in "${audit_dirs[@]}"; do
if [ -n "$category" ]; then
[ -f "$d/$category.jsonl" ] && files+=("$d/$category.jsonl")
else
for f in "$d"/*.jsonl; do [ -f "$f" ] && files+=("$f"); done
fi
done
shown=0
for f in "${files[@]}"; do
[ -f "$f" ] || continue
while IFS= read -r line; do
[ -z "$line" ] && continue
# kind filter (field-based; tolerates extra fields)
if [ -n "$kind" ] && ! printf '%s' "$line" | grep -q "\"kind\":\"${kind}\""; then
continue
fi
# since filter (ISO-8601 lexicographic compare on ts)
if [ -n "$since_iso" ]; then
ts=$(printf '%s' "$line" | grep -oE '"ts":"[^"]+"' | head -1 | sed 's/.*"ts":"//; s/"$//')
[ -n "$ts" ] && [ "$ts" \< "$since_iso" ] && continue
fi
printf '%s\t%s\n' "$(basename "$f" .jsonl)" "$line"
shown=$((shown + 1))
[ "$shown" -ge "$limit" ] && break 2
done < "$f"
done
[ "$shown" -eq 0 ] && echo "_No matching audit records._"
The skill leans on audit_count / audit_days_ago semantics already defined in bin/_audit.sh; it can also source that helper if richer counting is needed (source bin/_audit.sh; audit_count jobs job_begin).
Integration
Reads:
.claude/runtime/audit/<category>.jsonl(repo events — checked first)~/.lintel/audit/<category>.jsonl(operator events: pack lifecycle, pack-resolver, migrations,usage-*)
Writes:
- nothing (pure read)
Calls into:
bin/_audit.shhelpersaudit_count/audit_days_ago(optional, for counting)
Anti-patterns
- Editing the .jsonl logs by hand — they're append-only event trails. Don't rewrite history.
- Using this to drive control flow — it's a forensic reader, not a gate. Gates live in hooks.
- Re-implementing the JSON shape elsewhere — the schema is owned by
bin/_audit.sh. Read it through this skill.
See also
bin/_audit.sh(the unified writer —audit_log <category> <kind> [k=v ...])/li:status(in-flight jobs, reads_active.mdnot the audit log)/li:jobs(the jobs lifecycle controller — producesjobs.jsonlrecords)tests/shape/audit-writes-via-helper.sh(guard: no inline JSONL writers bypass the helper)