Enforcement

Where a check sits decides what it can stop. Asqav has three tiers. The Claude Code hook sits in the harness, outside the model. Decorators, SDK adapters and framework callbacks sit inside your own code and see only the calls routed through them. A signed record lets anyone detect tampering later. 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. Two inputs crash the command with exit code 1 instead of 2: a deeply nested event, and one holding an integer over 4300 digits. Those calls run unless the hook sets onFailure (see Setup).

A verified observation receipt lets the call run. The hosted platform records a permit from an in-process hook as an observation, 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.

python
import asqav

asqav.init(api_key="sk_...")
agent = asqav.Agent.create("my-agent")

# Every action is signed and hash-chained
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.

python
# 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.

python
import asqav
from asqav.extras.langchain import AsqavCallbackHandler

asqav.init(api_key="sk_...")

# 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.

python
# 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.

python
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:

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.

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:

bash
pip install "asqav[cli,verify]"
export ASQAV_API_KEY="sk_..."
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.

json
{
  "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.

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.

bash
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 covers the rest.