agentleFS
0xmortuex0xmortuex/claude-code-skills/skills/trail-guard/SKILL.md

trail-guard

Skill0 starsChanged 4 months agoMITView on GitHub

Audit whether every state-mutating action in a codebase — not just the ones a developer remembered — actually writes an audit-log entry, by enumerating mutating routes/RPCs/admin actions from the routing layer itself (not by grepping for existing log calls), then checking whether the log call is reached on every exit path including denials and exceptions, and whether coverage is structurally enforced or opt-in per handler. Use when asked "do we actually log every admin action", "can we tell who did this from the audit log", "audit log completeness review", when adding a new admin/bulk-action endpoint, before a compliance/SOC2 audit, or after an incident where "who did this" had no answer in the logs.

The file

Files
SKILL.md

trail-guard

An audit log is only as good as its coverage, and coverage almost always starts as an afterthought: a developer writing a new admin endpoint adds a logAudit(...) call because they thought to, and the next developer writing the next endpoint either copies that pattern or doesn't. Nothing forces the second case to fail loudly — the endpoint works, tests pass, the feature ships. The gap is invisible until the one moment it matters: an incident, a dispute, or an auditor asks "who did this and when," and the answer is an empty result set instead of a row. A real, merged fix (ROCm/spur PR #870, spurctld) found this exactly: "Spur recorded 3 of ~38 user-initiated mutating actions in the txn log ... Auditing was opt-in per handler, so anything added next started unaudited." The same PR found a second, subtler gap in the three handlers that did have logging: a permission-denial path returned before reaching the audit call, so "the old code returned before its audit call, so those denials went unrecorded despite the docs claiming otherwise" — the exact case (someone tried and was refused) that a security review most wants a record of. Both gaps look like nothing from inside any single handler; they only show up when every mutating surface is enumerated and checked against the same bar.

Step 1: enumerate every mutating surface from the routing layer, not from existing log calls

Build the list of what should be audited by walking the thing that dispatches requests — the HTTP route table, the RPC/gRPC service definition, the GraphQL schema's mutation type, the queue-consumer registrations, the admin-panel action list — not by grepping for audit, log, or existing calls to a logging function. Grepping for existing calls only finds handlers someone already remembered to instrument; it can't find the ones nobody did, which are the entire point of this audit. Classify each entry as mutating (creates/updates/deletes state, or has an irreversible side effect like sending an email or charging a card), read-only, or internal/system-only — a non-GET HTTP verb or a schema mutation type is a strong signal but not definitive (a POST /search is a read; a cron job calling an internal method directly bypasses the route table entirely and needs its own check).

Step 2: for each mutating surface, check whether the log call is reached on every exit path

A handler with a logAudit(...) call somewhere in its body is not the same as a handler that always logs. Check the specific ways a log call gets skipped without anyone noticing:

  • An early return before the log call — a permission check, a validation failure, or a "nothing changed" short-circuit that returns before reaching the logging line placed later in the function. This is the spur denial-path bug exactly, and it's the highest-value gap to find: a refused attempt is often more interesting to a later investigation than a successful one.
  • An exception between the mutation and the log call — if the write happens first and the audit call is a separate statement afterward, anything that throws in between (a downstream service timeout, a serialization error) leaves the mutation applied but unrecorded.
  • A transaction/log ordering mismatch — a log write that commits even though the surrounding database transaction later rolls back (a false-positive audit entry for something that didn't actually happen), or the reverse (the mutation commits but the log write is fire-and-forget and silently fails). Prefer the log write and the mutation committing atomically, or the log write happening from something that only runs after a confirmed commit.
  • A caught-and-swallowed logging failure — a try/catch around the audit call itself that logs an error and continues, which quietly turns "always audited" into "audited when the audit service happens to be up."

Step 3: check whether coverage is structural or opt-in

This is the root-cause question, and it's what separates a one-time cleanup from a fix that stays fixed. If the mechanism is "each handler calls a logging function," coverage degrades by default every time someone adds a new mutating endpoint without knowing this convention exists — exactly the failure mode spur had before its fix. Look for whether logging is instead enforced at a layer every request already passes through regardless of the individual handler author's awareness: middleware/interceptor wrapping the router, a decorator required by a lint rule or type system, or (the strongest form, and what spur shipped) a completeness test that fails the build when a newly-registered mutating route isn't classified. If coverage is opt-in today, say so as the primary finding rather than listing each currently-missing handler as an independent bug — the missing handlers are symptoms; the opt-in mechanism is the disease, and it will keep producing new symptoms.

Step 4: check bulk, admin-impersonation, and system-generated mutations specifically

These are the second most common gap after opt-in coverage, because they're often implemented as a separate code path from the single-record action that has the audit call — a bulk-delete endpoint calling the ORM's batch-delete method directly instead of looping through the single-delete handler, or a scheduled job that mutates rows without ever going through a route at all. Check each bulk/admin/system path found in Step 1 against Step 2's same bar independently; don't assume it inherits the single-record handler's logging just because it does the same kind of mutation.

Report

Lead with the finding that determines whether the audit log can be trusted at all: if coverage is opt-in with no enforcement (Step 3), say that first and treat every specific gap found in Steps 2 and 4 as evidence for it, not as isolated items. BLOCK for a mutating surface with no audit coverage at all on an admin action, a bulk/irreversible action, or anything touching regulated or sensitive data. BLOCK for a denial-path or exception-path gap on any of those same categories — an unrecorded refused attempt is a real security-relevant gap, not a nice-to-have. CAUTION for a coverage gap on a low-stakes, easily-reversible mutation, or for a structural (opt-in) finding where every currently-existing handler happens to be covered today. Cite the exact route/handler and the exact line where the exit path skips the log call — this is a completeness audit, so vague "consider adding more logging" advice defeats its purpose.

Boundaries

  • Doesn't cover the opposite direction — sensitive data (PII, secrets, tokens) leaking into logs, including audit logs. That's secret-spill's and security-sweep's territory; this skill only checks that a log entry exists, not what's safe to put in it.
  • Doesn't design the audit log's storage, retention, immutability, or tamper-resistance — only whether the application code actually calls into whatever logging mechanism exists. A perfectly durable, tamper-proof log that's opt-in and missing 35 of 38 handlers has the same practical gap as no log at all.
  • Not a diff review — security-sweep covers whether this change is exploitable; this skill is a whole-surface sweep like clock-sweep or portability-audit, meant to be run across a codebase (or a whole module/service) rather than one PR at a time, though a new endpoint being added is a fine trigger to check just that one surface.
  • Read-only access to another user's sensitive data can be a genuine audit-log gap too (regulatory audit trails often require it), but this skill's default lens is mutating actions — flag a read-access logging requirement only when the codebase's own compliance posture already calls for one, rather than asserting it as a universal rule.
  • Can't verify that a log call which executes in code actually reaches durable storage in production — a correct call into a logging client that's misconfigured, rate-limited, or pointed at a dead endpoint downstream is a delivery-pipeline question outside what static/code-level review can confirm.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.