← All recipes
IntegrationBeginner
Sign Claude tool-call outputs
When an agent loop runs multiple tool calls per task, each tool response is a separate AI artifact that may need to be verified later. Sign each one with its own receipt so the chain of tool calls is independently auditable.
When to use this pattern
- — Compliance-sensitive agent workflows (legal research, healthcare triage, financial analysis) where regulators may ask "which tool produced which result"
- — Multi-step pipelines where a downstream system trusts upstream tool outputs but needs proof later
- — Customer-facing agent products where each tool response is itself a deliverable
Pattern
Inside the agent loop, after Claude returns a tool_use block and your code executes the tool, sign the tool's response before sending it back to Claude as a tool_result:
import Anthropic from '@anthropic-ai/sdk'
import { CertNode } from '@certnode/sdk'
import crypto from 'crypto'
const claude = new Anthropic()
const cert = new CertNode({ apiKey: process.env.CERTNODE_API_KEY! })
async function runAgentTurn(userMessage: string) {
const tools = [
{ name: 'lookup_patient_record', /* ... schema ... */ },
{ name: 'search_drug_interactions', /* ... schema ... */ },
]
let messages: any[] = [{ role: 'user', content: userMessage }]
const receipts: string[] = []
while (true) {
const response = await claude.messages.create({
model: 'claude-opus-4-7',
max_tokens: 4096,
tools,
messages,
})
if (response.stop_reason === 'end_turn') {
// Sign the final assistant reply too
const finalText = response.content.find((c: any) => c.type === 'text')?.text ?? ''
if (finalText) {
const signed = await cert.signAIOutput({
output: finalText,
model: 'claude-opus-4-7',
provider: 'anthropic',
})
receipts.push(signed.receiptId)
}
return { reply: response, receiptIds: receipts }
}
if (response.stop_reason !== 'tool_use') break
// Process every tool_use block in this response
const toolResults: any[] = []
for (const block of response.content) {
if (block.type !== 'tool_use') continue
// Run the tool
const result = await runTool(block.name, block.input)
// Sign the tool result with metadata identifying tool + caller
const argsHash = crypto
.createHash('sha256')
.update(JSON.stringify(block.input))
.digest('hex')
const signed = await cert.signAIOutput({
output: JSON.stringify(result),
model: 'claude-opus-4-7',
provider: 'anthropic',
// promptHash carries the tool-call context (tool name + args hash)
promptHash: `tool=${block.name}|args=${argsHash}`,
})
receipts.push(signed.receiptId)
toolResults.push({
type: 'tool_result',
tool_use_id: block.id,
content: JSON.stringify(result),
})
}
// Add assistant's tool_use turn + our tool_results back into the conversation
messages.push({ role: 'assistant', content: response.content })
messages.push({ role: 'user', content: toolResults })
}
return { reply: null, receiptIds: receipts }
}What gets signed
Each tool response becomes its own signed receipt with:
- Content hash — sha256 of the JSON-stringified tool response
- Model + provider — the calling model identifier (Claude opus-4-7 in this example)
- Prompt hash field — encoded as
tool=<name>|args=<sha256>so verifiers can reconstruct which tool with which arguments produced the result - Three-layer timestamp — CertNode + RFC 3161 + Bitcoin OpenTimestamps (anchored within 1-2 hours)
Verifying later
If a regulator or counsel asks "which tool produced the medication recommendation":
// Look up the receipt
const receipt = await cert.verify({ receiptId: '...' })
// Extract tool name + args hash from promptHash
const [toolPart, argsPart] = receipt.metadata.promptHash.split('|')
const toolName = toolPart.replace('tool=', '')
const argsHash = argsPart.replace('args=', '')
// Re-hash the suspected arguments — does it match?
const suspectedArgsHash = crypto
.createHash('sha256')
.update(JSON.stringify(suspectedArgs))
.digest('hex')
console.log(suspectedArgsHash === argsHash) // true if these were the actual args
console.log(receipt.signedAt) // when the tool fired
console.log(receipt.timestamps.bitcoin?.status) // 'anchored' or 'pending'Notes
- — The free tier (100 receipts/mo) burns fast on agent loops. A single 5-tool turn produces 6 receipts (5 tools + 1 final reply). Plan capacity accordingly.
- — If a tool returns sensitive data (PHI, PII), sign the response BEFORE redaction so the receipt verifies the original tool output, then send the redacted version downstream.
- — Tool calls with side effects (database writes, API mutations) should sign AFTER the side effect succeeds — otherwise a successful receipt could exist for a tool call that never actually ran.