Enforcement
Updated
Asqav has three tiers: a Claude Code hook outside the model, decorators, SDK adapters and framework callbacks that see only calls routed through them, and signed records. Whichever tier makes the call, Asqav signs the outcome server-side into a tamper-evident receipt, recorded by a party that is not the agent's operator.
Three-level model
| Tier | What it does | Where it runs |
|---|---|---|
| Strong | Blocks the tool call when it cannot get a signed receipt it can verify. In asqav 0.10.10 a verified observation receipt, which is what the hosted platform returns for this hook, lets the call run. | The Claude Code harness, through asqav hook pretool |
| Bounded | Signs the calls routed through it. @asqav.sign and @asqav.secure run the function only after signing succeeds. SDK adapters and framework callbacks log an Asqav API error and let the call run, but a network error or a detector block can raise out of the adapter. |
Python decorators, SDK adapters and framework callbacks |
| Detectable | Tampering with a signed record is cryptographically provable | Agent.sign() |
Only the hook runs outside your agent's code. The bounded tier stops or records only the code that routes through it. Everything gets the detectable layer regardless.
Strong enforcement
This section describes the hook in asqav 0.10.10, the release that pip install "asqav[cli,verify]" installs from PyPI.
Claude Code runs asqav hook pretool before each matched tool call, so it does not depend on the model choosing to call it. The command sends the call to Asqav to be signed, checks the receipt that comes back against Asqav's published keys, and exits with code 2, which Claude Code treats as a block, when it cannot get a receipt it can verify. That covers an unreachable signer, a signing error, a missed deadline (5 seconds by default), missing credentials, an empty or non-JSON event and a bad signature. A verified receipt that carries a deny or rate_limit decision blocks too. Some inputs the command cannot parse or decode crash it with exit code 1 instead of 2: a deeply nested event, an integer over 4300 digits, and bytes that are not valid in the system's input encoding, which can include ordinary non-ASCII text where that encoding is not UTF-8. Those calls run unless the hook sets onFailure, which closes this gap (see Setup).
A verified observation receipt lets the call run. The hosted platform records the hook's permit as an observation, because the hook reports it from the machine the agent runs on, so in asqav 0.10.10 the hook works as a signing requirement: a call runs once it has a signed receipt that verifies. It does not wait for an allow decision.
Its reach is narrow. It sees only the tool calls Claude Code makes while the hook is installed and running, and some settings switch it off. Claude Code picks up edits to settings files while a session runs, so a tool call that is allowed to write a settings file can turn the hook off mid-session.
disableAllHooks turns hooks off, and the value that applies is the one left after settings precedence, where a project's .claude/settings.json overrides your user settings. --settings '{"disableAllHooks": true}' turns them off for one run. Managed settings are the exception: disableAllHooks set elsewhere cannot disable managed hooks, and allowManagedHooksOnly blocks user, project and local hooks. Setup below covers what happens when the hook fails to start.
Bounded enforcement
The @asqav.sign and @asqav.secure decorators sign a function call before the function body runs. If signing is refused, because of a blocking policy, a detector block, a non-2xx response or a network error, the error propagates and the body does not run. They fail closed, but they run inside your process and cover only the functions you decorate.
SDK adapters and framework callbacks sign the calls that reach them. A wrapper such as wrap_tool in the smolagents adapter replaces the tool's forward, so with the default local executor every call to that tool goes through it. A framework callback, such as LangChain's, only sees the calls the framework routes through its callback path. A call made outside either path is not checked or signed. If the Asqav API returns an error, the adapter logs it and the call goes ahead. A network error or a detector block is not caught by the shared adapter code and raises out of it. Whether that stops the call depends on the adapter: the smolagents wrapper catches it and carries on, while the Haystack component has no handler around its signing call in run(). Use adapters for an audit trail of the calls they see, not as a gate. The capture coverage table in the asqav-sdk README lists, adapter by adapter, how each one observes the interaction it signs.
Detectable enforcement
Agent.sign() creates a signed record of the submitted action. ML-DSA protects the signed bytes; hash-chain links let a verifier check continuity between records. Detecting an omitted tail or actions that were never submitted also requires a trusted checkpoint or an independent record of the expected activity.
import asqav
asqav.init()
agent = asqav.Agent.create("my-agent")
# Each sign() returns a receipt, hash-chained in compliance mode (the SDK default)
sig1 = agent.sign("data:read", {"table": "users"})
sig2 = agent.sign("data:write", {"table": "users", "rows": 5})
# Check the receipt signature
result = asqav.verify_signature(sig2.signature_id)
print(result.verification_detail.signature_valid)
Three-phase signing
The sign_with_phases helper signs an intent, requests a preflight decision, and signs an execution-labelled record if the decision clears. The decision is a preflight response, not a separate signed receipt. The helper does not call your tool: its execution record contains the supplied context, not an independently observed outcome.
# Three-phase signing: intent, decision, execution
chain = agent.sign_with_phases("data:delete", {"table": "users"})
print(chain.approved) # True if policy allowed it
print(chain.intent) # signed intent receipt
print(chain.decision) # policy evaluation result
print(chain.execution) # signed execution receipt (only if approved)
If preflight denies the action, chain.execution is None. Your executor must honor that result. Record the actual tool outcome after execution if you need evidence of what happened.
Observe mode
For SDK callback handlers, observe=True logs the actions and context that would be submitted for signing. It does not simulate policy evaluation or block the tool. Initialization still needs authentication and can create or retrieve an agent; session callbacks can also call the API. Use non-sensitive test data, because the local log includes context.
import asqav
from asqav.extras.langchain import AsqavCallbackHandler
asqav.init()
# observe=True logs what would be signed without signing or blocking
handler = AsqavCallbackHandler(agent_name="my-agent", observe=True)
# Log output: OBSERVE: would sign tool:search with context {...}
Semantic action patterns
The SDK ships named semantic patterns that resolve to glob equivalents, so a policy can target a category of actions without hand-writing the glob.
# Resolve a named pattern to its glob
import asqav
asqav.resolve_pattern("sql-destructive") # "data:delete:*"
asqav.list_patterns() # every built-in pattern name and its glob
# Use the resolved glob as the policy's action_pattern
# (POST /api/v1/policies, see the Policies page for the full schema)
The available pattern names are listed in the Policies reference.
Preflight explanations
A preflight check returns a plain-English explanation alongside the cleared/blocked outcome, so a denial is never a bare boolean.
result = agent.preflight("database:delete:users")
print(result.cleared) # False
print(result.explanation) # human-readable reason
Fail-closed semantics
Fail closed means the call does not run when signing fails. The paths differ:
asqav hook pretoolblocks the call when it cannot get a receipt it can verify. A hook that cannot start, or that crashes with exit code 1 on input it cannot parse or decode, does not block unless you setonFailure(see Setup).@asqav.signand@asqav.securedo not run the function when signing is refused, including by a detector block.- SDK adapters and framework callbacks log an Asqav API error and let the call run, and that call has no receipt. A network error or a detector block raises out of the shared adapter code, and the call stops only where the adapter or the framework does not catch it.
asqav hook posttoolfails open and runs after the tool, so it can never stop a call.
Claude Code hook
The SDK ships two Claude Code hook commands. asqav hook pretool is the gate and runs before the tool. asqav hook posttool is the audit and runs after it, so it records the call and cannot stop it. A signed decision receipt records a decision the gate made; it does not prove the tool call ran, succeeded, or had any particular effect: the receipt records the decision, not the side effect. An audit receipt records only that the call was observed.
Setup
Install the SDK with both extras. The cli extra provides the command. Without it the command exits 1, and Claude Code lets the call run on exit code 1 unless the hook sets "onFailure": "block" (v2.1.295 or later), as in the example below. The verify extra provides the signature check the gate runs on the receipt. The hook signs as one agent, set by two environment variables:
pip install "asqav[cli,verify]"
export ASQAV_API_KEY=<paste your key here>
export ASQAV_AGENT_ID="your-agent-id"
Wire the gate into the PreToolUse event of your Claude Code settings.json. Set the hook's timeout a little above the gate's own deadline.
{
"hooks": {
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "asqav hook pretool",
"timeout": 10,
"onFailure": "block"
}
]
}
]
}
}
By default Claude Code blocks a PreToolUse call on exit code 2. A hook that exits with any other code, fails to start or times out lets the call run. "onFailure": "block" makes a hook that cannot start, times out or exits with a code other than 0 or 2 block instead, and it needs Claude Code v2.1.295 or later. A hook can also block by exiting 0 with a JSON deny decision, but asqav hook pretool uses exit code 2.
Agent SDK callback hooks carry their own timeout rule at the framework seam: an Agent SDK callback hook that exceeds its timeout blocks the tool call, per the Agent SDK hooks reference. That is the framework's own rule, distinct from the harness-hook "onFailure" setting above.
The capture coverage table in the asqav-sdk README lists Claude Code 2.1.278 for the hook. That release is older than onFailure, which the Claude Code reference documents from v2.1.295, so the example above needs a newer Claude Code than that table lists.
Run the exact command string from your settings once and read the exit code back. An empty event is blocked on purpose, so a working gate answers 2. Any other code means the gate is not enforcing.
sh -c 'asqav hook pretool' </dev/null; echo "exit=$?"
For the audit, wire asqav hook posttool into the PostToolUse event the same way. The hooks reference for 0.10.10 in the asqav-sdk repository describes the hook as 0.10.10 ships it.
References
- FIPS 204: Module-Lattice-Based Digital Signature Standard, NIST
- Hooks reference, Anthropic