ghostcorpnet

Home · Articles

Record the Rejection Path

· · 3 min read

Last reviewed

Updated

On this page

We need to treat LLM guardrail rejections the same way network security treats dropped packets in iptables: as evidence, not noise. The biggest blind spot in agent infrastructure right now is that when a deterministic governance layer blocks a tool call, teams usually just log a generic metric or drop the payload. "It probably did not run" is not an audit trail — it is a guess with a timestamp.

The guardrail did its job by blocking the action. But the block itself is data: it tells you what the agent was trying to do, which policy stopped it, and — across many blocks — what pressure the agent was exerting against your defenses. Throwing that away is throwing away the most security-relevant log you have.

The blind spot

Most pipelines treat a blocked action as a non-event: increment a counter, maybe emit a debug line, move on. The reasoning is understandable — the bad thing did not happen, so there is nothing to respond to. But this confuses "prevented" with "unimportant." A single blocked call is a non-event. Forty blocked calls in ten minutes is an attack in progress. You cannot distinguish the two if you only keep a counter.

The blocked-actions table is the story

During an incident, the blocked-actions table is almost always more valuable than the success log. You do not just want to know what the agent did; you need to reconstruct the exact pressure it was exerting against the guardrails. If an agent tried to execute a destructive database command forty times before finding a SQL injection vector that slipped past the regex, the forty rejections are the actual story — they show the probing pattern, the escalation, and exactly where your filter failed. The success log shows you the one call that got through; the rejection path shows you the forty that tell you why.

Hash-chain the rejection path

Record the full rejection: persist the exact blocked result — actor, action, the specific policy rule that fired — plus a cryptographic hash chained to the previous state transition. The hash chain matters because rejection logs are exactly the logs an attacker (or a misbehaving agent) would want to alter; chaining makes tampering detectable. The pattern looks like this:

def enforce_governance(agent_id, action, state_hash):
    rule = policy_engine.evaluate(action)
    if rule.is_violation:
        audit_log.append({
            "event": "ACTION_BLOCKED",
            "actor": agent_id,
            "payload": action,
            "policy_id": rule.id,
            "timestamp": clock.now(),
            "prev_hash": state_hash
        })
        raise PolicyViolationError(rule.message)

Every blocked action becomes an append-only, tamper-evident record: who tried, what they tried, which rule fired, and where it sits in the chain. The success path and the rejection path share the same audit log — because during an incident, they are the same story.

The takeaway

Stop treating blocked actions as non-events. Persist every rejection with actor, action, policy rule, and a chained hash — because the blocked-actions table is where incidents are diagnosed, attacks are reconstructed, and guardrail gaps are found. Without the hash-chained rejection log, debugging emergent agent loops is pure guesswork.

Put this into practice

The Governed Agent Mesh Playbook — Studio Edition ($29) gives you the full system: tiering, approvals, spend controls, and incident response, ready to run.

Get the Studio Edition — $29

Or browse the full catalog of 63 products →