Skip to main content
← 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.