Sign user prompts with privacy-preserving hashes
Sometimes you need the receipt to prove what a user asked — but the prompt itself contains PHI, PII, attorney-client privileged content, or sensitive trade information you can't ship to a third-party signer. Hash first, sign the hash.
The privacy guarantee
CertNode receipts include an optional promptHash field. The Sign API never sees the raw prompt — only the sha256 you pre-compute. The hash is one-way: nobody can recover the prompt from the hash, but you (or anyone you show the original prompt to later) can verify the hash matches.
The AI output itself goes through CertNode (you need the actual content signed). If the output is also sensitive, see the alternative pattern below using a salted hash of the output.
Pattern: hash the prompt, sign the output
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 answerSensitivePrompt(sensitivePrompt: string, userId: string) {
// 1. Hash the prompt FIRST — CertNode never sees the raw content
const promptHash = crypto
.createHash('sha256')
.update(sensitivePrompt)
.digest('hex')
// 2. Call the model (this still happens on YOUR infrastructure or your AI
// provider — CertNode is not in this path)
const response = await claude.messages.create({
model: 'claude-opus-4-7',
max_tokens: 2048,
messages: [{ role: 'user', content: sensitivePrompt }],
})
const aiOutput = response.content.find((c: any) => c.type === 'text')?.text ?? ''
// 3. Sign the OUTPUT, attach the PROMPT HASH (not the prompt)
const signed = await cert.signAIOutput({
output: aiOutput,
model: 'claude-opus-4-7',
provider: 'anthropic',
promptHash, // sha256 of the prompt — irreversible, no privacy exposure
})
// 4. Store prompt + receipt link in YOUR database
// (you control retention + access; CertNode just has the hash)
await db.promptLog.insert({
userId,
prompt: sensitivePrompt, // still need to keep this somewhere
promptHash, // for fast lookup by hash
receiptId: signed.receiptId,
signedAt: signed.signedAt,
})
return { answer: aiOutput, receiptId: signed.receiptId }
}Verifying "did this user really ask this?"
When someone challenges the chain ("I never asked that," or "this output wasn't for this prompt"), the proof is a hash comparison:
async function verifyPromptBinding(receiptId: string, claimedPrompt: string) {
// 1. Pull the receipt
const receipt = await cert.verify({ receiptId })
if (!receipt.valid) return { valid: false, reason: 'receipt_invalid' }
// 2. Hash the claimed prompt
const claimedHash = crypto
.createHash('sha256')
.update(claimedPrompt)
.digest('hex')
// 3. Compare
if (claimedHash !== receipt.metadata.promptHash) {
return { valid: false, reason: 'prompt_does_not_match' }
}
return {
valid: true,
signedAt: receipt.signedAt,
bitcoinAnchored: receipt.timestamps.bitcoin?.status === 'anchored',
}
}The user (or counsel, regulator, auditor) can produce the claimed prompt, hash it, compare. Match = proven binding. Mismatch = the prompt has been altered or the user is presenting the wrong prompt for this receipt.
When the OUTPUT is also sensitive
For maximum privacy: sign a sentinel string + include both prompt-hash and output-hash in metadata. The receipt then attests to the hashes; you keep the content private entirely.
// Salted hashes — adds a per-org secret to prevent rainbow-table attacks
const SALT = process.env.PROVENANCE_HASH_SALT! // 32 random bytes, stored in your secrets
const promptHash = crypto.createHash('sha256').update(SALT + sensitivePrompt).digest('hex')
const outputHash = crypto.createHash('sha256').update(SALT + aiOutput).digest('hex')
// Sign a sentinel string — the receipt's content hash is over this, not the
// real content. The real content is referenced by the salted hashes in
// metadata.
const signed = await cert.signAIOutput({
output: `<sealed-content prompt-hash="${promptHash}" output-hash="${outputHash}" model="claude-opus-4-7" />`,
model: 'claude-opus-4-7',
provider: 'anthropic',
promptHash,
})
// Store the actual prompt + output in your own encrypted storage,
// keyed by receiptId. Verification is then a 3-way check:
// 1. CertNode confirms receipt validity + timestamp chain
// 2. You produce the prompt + output from your storage
// 3. Re-hash with salt, compare to receipt metadataWhen to use which pattern
- Prompt sensitive, output not: hash the prompt only. The output stored in the receipt is queryable by hash. This is the most common case.
- Both sensitive: use the sealed-content pattern with salted hashes. The receipt becomes a binding ledger entry, not a content store.
- Neither sensitive: use the default pattern without promptHash. Signing the output is enough; the receipt is queryable for public verify.
Privacy compliance notes
- — Hashes are not personal data under GDPR/CCPA if the original content can't be recovered from them. Salted hashes are even stronger because rainbow-table attacks fail.
- — HIPAA: hashing PHI before transmission means CertNode is not a Business Associate under HIPAA — you never disclose PHI to us. Document this in your BAA risk assessment.
- — Attorney-client privilege: salted-hash pattern preserves privilege because no privileged content reaches CertNode. Confirm with your firm's ethics counsel.