DEV Community

willamhou
willamhou

Posted on

How to Add Tamper-Evident Audit Trails to Your CrewAI Agents

Your CrewAI crew kicks off a task. Agents delegate to each other, call tools, return results. But can you prove what each agent actually did?

CrewAI's built-in logs capture what happened. Cryptographic receipts prove it. The difference matters when an auditor, a customer, or a regulator asks "show me exactly what the agent did and prove it wasn't altered after the fact."

This tutorial adds Ed25519-signed, hash-chained audit trails to a CrewAI crew in under 5 minutes. Signet itself needs no external service, no signing API, no infrastructure — receipts verify offline with a public key. (Your CrewAI agent and any tools it uses still need their own keys; that part is unchanged.)

What you'll build

A CrewAI crew where every tool call produces a signed receipt containing:

  • What: which tool was called, with what parameters
  • Who: the agent's Ed25519 public key
  • When: timestamp
  • Proof: Ed25519 signature over JCS-canonicalized (RFC 8785) payload
  • Chain: SHA-256 hash linking to the previous receipt (tamper-evident ordering)

If anyone modifies a receipt after the fact, the signature breaks. If anyone deletes or reorders receipts, the hash chain breaks.

Install

pip install signet-auth[crewai] crewai-tools
Enter fullscreen mode Exit fullscreen mode

Requires a CrewAI release that exposes the crewai.hooks global tool-hook API (tested with the current PyPI release). crewai-tools ships the SerperDevTool used below; it is not bundled into the [crewai] extra.

SerperDevTool calls Serper's web-search API and the CrewAI agent itself calls an LLM provider, so this exact demo also wants:

export OPENAI_API_KEY="sk-..."
export SERPER_API_KEY="..."
Enter fullscreen mode Exit fullscreen mode

If you'd rather run zero-key, swap SerperDevTool() for any local tool — Signet signs whatever flows through crewai.hooks regardless of what the tool does.

Step 1: Create a signing identity

from signet_auth import SigningAgent, KeyNotFoundError

# Load an existing Ed25519 identity, or create one on first run.
# Keys live at ~/.signet/keys/ — the private key stays local.
try:
    agent = SigningAgent("my-crewai-agent")
except KeyNotFoundError:
    agent = SigningAgent.create("my-crewai-agent", owner="acme-corp")

print(f"Public key: {agent.public_key}")
Enter fullscreen mode Exit fullscreen mode

No key server, no certificate authority. The private key stays on disk, the public key is what verifiers use.

Step 2: Install the signing hooks

CrewAI exposes global tool hooks. Signet plugs into them with one call:

from signet_auth.crewai import install_hooks

install_hooks(agent)
Enter fullscreen mode Exit fullscreen mode

That's it. Every tool call across every agent in every crew is now signed automatically.

The hooks cover the full lifecycle: before_tool_call (what was called, signed before the tool runs) and after_tool_call (what was returned, hashed and signed). Recoverable signing errors (SignetError — bad payload, audit-log IO trouble) are caught and logged as a warning. Programmer errors that imply your code is in a bad state (e.g. calling agent.close() and then continuing to use the agent) still raise — by design, so you find them at dev time rather than silently dropping receipts in production.

Step 3: Run your crew normally

from crewai import Agent, Crew, Task
from crewai_tools import SerperDevTool

researcher = Agent(
    role="Research Analyst",
    goal="Find information on a given topic",
    backstory="Experienced analyst with attention to detail",
    tools=[SerperDevTool()],
)

task = Task(
    description="Research the weather in Tokyo",
    expected_output="A summary of today's weather",
    agent=researcher,
)

crew = Crew(agents=[researcher], tasks=[task])
result = crew.kickoff()
Enter fullscreen mode Exit fullscreen mode

No changes to your agents, tools, or crew. The signing happens transparently through the hooks.

Step 4: Inspect the receipts

import json
from signet_auth.crewai import get_receipts

for i, receipt in enumerate(get_receipts()):
    data = json.loads(receipt.to_json())
    print(f"\nReceipt #{i+1}")
    print(f"  Tool:       {data['action']['tool']}")
    print(f"  Params hash: {data['action']['params_hash']}")
    print(f"  Signature:   {data['sig'][:40]}...")
    print(f"  Timestamp:   {data['ts']}")
Enter fullscreen mode Exit fullscreen mode

Output:

Receipt #1
  Tool:       serper_dev
  Params hash: sha256:a1b2c3...
  Signature:   ed25519:Mz4xNTk2NjQ0NDgw...
  Timestamp:   2026-04-20T14:30:00Z

Receipt #2
  Tool:       _tool_end
  Params hash: sha256:d4e5f6...
  Signature:   ed25519:Nk5yODk3MjE1Njg4...
  Timestamp:   2026-04-20T14:30:02Z
Enter fullscreen mode Exit fullscreen mode

Notice the start/end pair: the first receipt captures the tool call, the second captures a SHA-256 hash of the output. Together they prove what was called and the hash of what it returned. (The full output stays in your application; the receipt only signs the hash, which is enough to detect any after-the-fact tampering of an output you've stored elsewhere.)

Step 5: Verify a receipt

Anyone with the public key can verify, offline:

from signet_auth import verify

receipts = get_receipts()
receipt = receipts[0]

is_valid = verify(receipt, agent.public_key)
print(f"Valid: {is_valid}")  # True
Enter fullscreen mode Exit fullscreen mode

Tamper with any field and verification fails:

data = json.loads(receipt.to_json())
data["action"]["tool"] = "evil_tool"  # tamper

from signet_auth import Receipt
tampered = Receipt.from_json(json.dumps(data))
print(verify(tampered, agent.public_key))  # False
Enter fullscreen mode Exit fullscreen mode

Step 6: Verify the audit chain

The audit log is a hash-chained JSONL file. Each entry's hash covers the previous entry, so deleting or reordering receipts breaks the chain:

from signet_auth import audit_verify_chain, default_signet_dir

signet_dir = default_signet_dir()
chain_status = audit_verify_chain(signet_dir)
print(f"Chain intact: {chain_status.valid}")
print(f"Entries: {chain_status.total_records}")
Enter fullscreen mode Exit fullscreen mode

Step 7: Clean up (optional)

When you're done signing, uninstall the hooks:

from signet_auth.crewai import uninstall_hooks

uninstall_hooks()
Enter fullscreen mode Exit fullscreen mode

Why this matters for CrewAI specifically

CrewAI's strength is agent-to-agent delegation. A researcher agent delegates to a writer agent, who calls tools, who returns results that feed back into the chain. When something goes wrong, "which agent did what" becomes a real question.

Signed receipts answer that question independently of CrewAI's own logs. CrewAI's ToolCallHookContext does expose agent to the hook today, but Signet's current install_hooks() binding ties one signing identity to the global hook registration; per-call routing to a per-agent key is on the roadmap. For now, if you need separate keys per agent, the practical pattern is: install with one agent's key, run that agent's task, call uninstall_hooks(), then re-install with the next agent's key.

Either way, the audit trail proves not just what happened but who signed it.

What this gives you

Without Signet With Signet
"The research agent called serper_dev" (log entry) Ed25519 signature proving it, verifiable by anyone with the public key
Logs can be edited after the fact Signature breaks if any field is modified
No ordering proof Hash chain breaks if receipts are deleted or reordered
Trust CrewAI's logs Verify independently, offline

When you need this

  • Regulated industries: EU AI Act Article 12 requires "automatic recording of events" for high-risk systems. Tamper-evident signed receipts can support that traceability + log-integrity requirement (the legal sufficiency check is your auditor's call, not Signet's — but they need something better than rotatable plaintext logs to point at).
  • Enterprise deployments: When the question is "can you prove what the agent did?", signed receipts are the answer.
  • Agent-to-agent: CrewAI's core pattern — when one agent verifies another's work, signatures make it cryptographic, not just log-based.
  • Incident response: After something goes wrong in a multi-agent crew, tamper-evident receipts let you reconstruct exactly what happened without trusting anyone's claim.

Next steps

  • Bilateral co-signing: Have both the agent and the tool server sign each interaction independently. Neither party can fabricate receipts. See signet proxy for MCP integration.
  • Policy attestation: Evaluate YAML policy rules and include the decision (allow/deny/require_approval) inside the signed receipt.
  • Delegation chains: Prove that Agent A was authorized by Human B to perform a specific action with scoped constraints. Useful when CrewAI agents are acting on behalf of specific users.

All of these are in signet-auth today.

pip install signet-auth[crewai]
Enter fullscreen mode Exit fullscreen mode

GitHub: Prismer-AI/signet


Signet is open source (Apache-2.0 OR MIT). Rust core with Python and TypeScript bindings. The signing layer needs no external service or signing API — receipts verify offline with the public key.

Top comments (0)