paperclipai/paperclip · error · GitHubAttachmentUnavailableError
github_attachment_canonical_body_mismatch
github_attachment_canonical_body_mismatch
Error message
github_attachment_canonical_body_mismatch
What it means
resolveCanonicalAttachmentTargetOrThrow verifies that the canonical comment body is the exact text the attachment was created from: row.body must be a string of at most 200,000 characters and its SHA-256 hex digest must equal locator.sourceBodySha256. If the body is missing, oversized, or its hash differs from the pinned hash, it throws github_attachment_canonical_body_mismatch, because the HTML rendering would no longer be evidence for the unchanged source the attachment points at.
Source
Thrown at server/src/services/chat-github-attachments.ts:568
const thread =
/^github:([^:]+):(?:(issue):)?([1-9][0-9]*)(?::rc:([1-9][0-9]*))?$/i.exec(
locator.sourceThreadId,
)!;
if (
String(row.id) !== locator.sourceMessageId ||
typeof row.url !== "string" ||
row.url.toLowerCase() !== request.url.toLowerCase()
)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_source_mismatch",
);
if (
typeof row.body !== "string" ||
row.body.length > 200_000 ||
createHash("sha256").update(row.body).digest("hex") !==
locator.sourceBodySha256
)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_body_mismatch",
);
if (typeof row.body_html !== "string" || row.body_html.length > 600_000)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_html_unavailable",
);
if (thread[4]) {
if (
row.pull_request_url !==
`https://api.github.com/repos/${thread[1]}/pulls/${thread[3]}` ||
String(row.in_reply_to_id ?? row.id) !== thread[4]
)
throw new GitHubAttachmentUnavailableError(
"github_attachment_canonical_source_mismatch",
);
} else if (
row.issue_url !==
`https://api.github.com/repos/${thread[1]}/issues/${thread[3]}`View on GitHub (pinned to 01ad858492)
Solutions
- Re-attach the image/file from the current version of the comment so sourceBodySha256 is recomputed against the edited body.
- Restore the comment body to its original content if the edit was unintentional (GitHub keeps edit history for comments).
- If bodies are legitimately large, confirm they stay under the 200,000-character limit or adjust the limit in your deployment.
- Check that no middleware rewrites the body field (e.g. HTML-escaping or whitespace normalization) between fetch and verification; compare hashes locally with `sha256sum` against the stored value.
Example fix
// before: verifying against a stale hash after the comment was edited const ok = sha256(row.body) === locator.sourceBodySha256; // fails silently every time // after: refresh the locator when the body legitimately changed const fresh = await captureAttachmentLocator(attachment.url); // recompute sha256 await updateAttachmentLocator(attachment.id, fresh);
Defensive patterns
Strategy: validation
Validate before calling
// Pre-verify the body hash before resolution
import { createHash } from 'node:crypto';
const digest = createHash('sha256').update(String(row.body ?? '')).digest('hex');
if (typeof row.body !== 'string' || row.body.length > 200_000 || digest !== locator.sourceBodySha256)
throw new Error('comment body changed or exceeds limit'); Type guard
function bodyIntact(row: Record<string, unknown>, expectedSha256: string): boolean {
return typeof row.body === 'string' &&
row.body.length <= 200_000 &&
createHash('sha256').update(row.body).digest('hex') === expectedSha256;
} Try / catch
const target = resolveGitHubCommentAttachmentTarget(attachment, value);
if (target === null) {
// body mismatch mapped to null: the source changed — require re-attachment, do not retry
notifyUserToReattach(attachment);
return null;
} Prevention
- Do not edit a comment after attaching files to it, or re-attach immediately after an edit.
- Store the SHA-256 of the raw body at attach time and compare before every resolution.
- Keep bodies under the 200,000-character cap; split oversized content into multiple comments.
- Ensure no middleware rewrites row.body (whitespace, HTML-escaping) between fetch and verification, which would break the hash for identical content.
When it happens
Trigger: The GitHub comment body was edited after the attachment locator recorded sourceBodySha256; row.body is absent or not a string in the API response; the comment body exceeds 200,000 characters; the body hash computation differs (e.g. body normalized/transformed between capture and verification).
Common situations: A user edited or reflowed the comment after the file was attached; re-resolution happens against an older/newer revision of the comment; a proxy or API version changes whitespace/encoding in body so the SHA-256 no longer matches; extremely long comments exceed the 200k cap.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- github_attachment_canonical_source_mismatch
- native_interaction_governed_request_unresolved
- runnerd digest mismatch: expected ${request.runnerd.sha256},
- Could not prepare managed GitHub launchers
- Materialized OpenCode executable digest mismatch
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/c8cad471951dbff5.
Report an issue: GitHub.