{"id":642,"date":"2026-03-21T22:52:58","date_gmt":"2026-03-21T14:52:58","guid":{"rendered":"https:\/\/pa.yingzhi8.cn\/index.php\/2026\/03\/21\/tools-diffs\/"},"modified":"2026-03-21T23:08:52","modified_gmt":"2026-03-21T15:08:52","slug":"tools-diffs","status":"publish","type":"post","link":"https:\/\/pa.yingzhi8.cn\/index.php\/2026\/03\/21\/tools-diffs\/","title":{"rendered":"Diffs"},"content":{"rendered":"<h1>Diffs<\/h1>\n<p><code>diffs<\/code> is an optional plugin tool with short built-in system guidance and a companion skill that turns change content into a read-only diff artifact for agents.<\/p>\n<p>It accepts either:<\/p>\n<ul>\n<li><code>before<\/code> and <code>after<\/code> text<\/li>\n<li>a unified <code>patch<\/code><\/li>\n<\/ul>\n<p>It can return:<\/p>\n<ul>\n<li>a gateway viewer URL for canvas presentation<\/li>\n<li>a rendered file path (PNG or PDF) for message delivery<\/li>\n<li>both outputs in one call<\/li>\n<\/ul>\n<p>When enabled, the plugin prepends concise usage guidance into system-prompt space and also exposes a detailed skill for cases where the agent needs fuller instructions.<\/p>\n<h2>Quick start<\/h2>\n<ol>\n<li>Enable the plugin.<\/li>\n<li>Call <code>diffs<\/code> with <code>mode: \"view\"<\/code> for canvas-first flows.<\/li>\n<li>Call <code>diffs<\/code> with <code>mode: \"file\"<\/code> for chat file delivery flows.<\/li>\n<li>Call <code>diffs<\/code> with <code>mode: \"both\"<\/code> when you need both artifacts.<\/li>\n<\/ol>\n<h2>Enable the plugin<\/h2>\n<p>&#8220;`json5  theme={&#8220;theme&#8221;:{&#8220;light&#8221;:&#8221;min-light&#8221;,&#8221;dark&#8221;:&#8221;min-dark&#8221;}}<br \/>\n{<br \/>\n  plugins: {<br \/>\n    entries: {<br \/>\n      diffs: {<br \/>\n        enabled: true,<br \/>\n      },<br \/>\n    },<br \/>\n  },<br \/>\n}<\/p>\n<pre><code>\n## Disable built-in system guidance\n\nIf you want to keep the `diffs` tool enabled but disable its built-in system-prompt guidance, set `plugins.entries.diffs.hooks.allowPromptInjection` to `false`:\n\n```json5  theme={&quot;theme&quot;:{&quot;light&quot;:&quot;min-light&quot;,&quot;dark&quot;:&quot;min-dark&quot;}}\n{\n  plugins: {\n    entries: {\n      diffs: {\n        enabled: true,\n        hooks: {\n          allowPromptInjection: false,\n        },\n      },\n    },\n  },\n}\n<\/code><\/pre>\n<p>This blocks the diffs plugin&#8217;s <code>before_prompt_build<\/code> hook while keeping the plugin, tool, and companion skill available.<\/p>\n<p>If you want to disable both the guidance and the tool, disable the plugin instead.<\/p>\n<h2>Typical agent workflow<\/h2>\n<ol>\n<li>Agent calls <code>diffs<\/code>.<\/li>\n<li>Agent reads <code>details<\/code> fields.<\/li>\n<li>Agent either:<br \/>\n   * opens <code>details.viewerUrl<\/code> with <code>canvas present<\/code><br \/>\n   * sends <code>details.filePath<\/code> with <code>message<\/code> using <code>path<\/code> or <code>filePath<\/code><br \/>\n   * does both<\/li>\n<\/ol>\n<h2>Input examples<\/h2>\n<p>Before and after:<\/p>\n<p>&#8220;`json  theme={&#8220;theme&#8221;:{&#8220;light&#8221;:&#8221;min-light&#8221;,&#8221;dark&#8221;:&#8221;min-dark&#8221;}}<br \/>\n{<br \/>\n  &#8220;before&#8221;: &#8220;# HellonnOne&#8221;,<br \/>\n  &#8220;after&#8221;: &#8220;# HellonnTwo&#8221;,<br \/>\n  &#8220;path&#8221;: &#8220;docs\/example.md&#8221;,<br \/>\n  &#8220;mode&#8221;: &#8220;view&#8221;<br \/>\n}<\/p>\n<pre><code>\nPatch:\n\n```json  theme={&quot;theme&quot;:{&quot;light&quot;:&quot;min-light&quot;,&quot;dark&quot;:&quot;min-dark&quot;}}\n{\n  &quot;patch&quot;: &quot;diff --git a\/src\/example.ts b\/src\/example.tsn--- a\/src\/example.tsn+++ b\/src\/example.tsn@@ -1 +1 @@n-const x = 1;n+const x = 2;n&quot;,\n  &quot;mode&quot;: &quot;both&quot;\n}\n<\/code><\/pre>\n<h2>Tool input reference<\/h2>\n<p>All fields are optional unless noted:<\/p>\n<ul>\n<li><code>before<\/code> (<code>string<\/code>): original text. Required with <code>after<\/code> when <code>patch<\/code> is omitted.<\/li>\n<li><code>after<\/code> (<code>string<\/code>): updated text. Required with <code>before<\/code> when <code>patch<\/code> is omitted.<\/li>\n<li><code>patch<\/code> (<code>string<\/code>): unified diff text. Mutually exclusive with <code>before<\/code> and <code>after<\/code>.<\/li>\n<li><code>path<\/code> (<code>string<\/code>): display filename for before and after mode.<\/li>\n<li><code>lang<\/code> (<code>string<\/code>): language override hint for before and after mode.<\/li>\n<li><code>title<\/code> (<code>string<\/code>): viewer title override.<\/li>\n<li><code>mode<\/code> (<code>\"view\" | \"file\" | \"both\"<\/code>): output mode. Defaults to plugin default <code>defaults.mode<\/code>.<br \/>\n  Deprecated alias: <code>\"image\"<\/code> behaves like <code>\"file\"<\/code> and is still accepted for backward compatibility.<\/li>\n<li><code>theme<\/code> (<code>\"light\" | \"dark\"<\/code>): viewer theme. Defaults to plugin default <code>defaults.theme<\/code>.<\/li>\n<li><code>layout<\/code> (<code>\"unified\" | \"split\"<\/code>): diff layout. Defaults to plugin default <code>defaults.layout<\/code>.<\/li>\n<li><code>expandUnchanged<\/code> (<code>boolean<\/code>): expand unchanged sections when full context is available. Per-call option only (not a plugin default key).<\/li>\n<li><code>fileFormat<\/code> (<code>\"png\" | \"pdf\"<\/code>): rendered file format. Defaults to plugin default <code>defaults.fileFormat<\/code>.<\/li>\n<li><code>fileQuality<\/code> (<code>\"standard\" | \"hq\" | \"print\"<\/code>): quality preset for PNG or PDF rendering.<\/li>\n<li><code>fileScale<\/code> (<code>number<\/code>): device scale override (<code>1<\/code>&#8211;<code>4<\/code>).<\/li>\n<li><code>fileMaxWidth<\/code> (<code>number<\/code>): max render width in CSS pixels (<code>640<\/code>&#8211;<code>2400<\/code>).<\/li>\n<li><code>ttlSeconds<\/code> (<code>number<\/code>): viewer artifact TTL in seconds. Default 1800, max 21600.<\/li>\n<li><code>baseUrl<\/code> (<code>string<\/code>): viewer URL origin override. Must be <code>http<\/code> or <code>https<\/code>, no query\/hash.<\/li>\n<\/ul>\n<p>Validation and limits:<\/p>\n<ul>\n<li><code>before<\/code> and <code>after<\/code> each max 512 KiB.<\/li>\n<li><code>patch<\/code> max 2 MiB.<\/li>\n<li><code>path<\/code> max 2048 bytes.<\/li>\n<li><code>lang<\/code> max 128 bytes.<\/li>\n<li><code>title<\/code> max 1024 bytes.<\/li>\n<li>Patch complexity cap: max 128 files and 120000 total lines.<\/li>\n<li><code>patch<\/code> and <code>before<\/code> or <code>after<\/code> together are rejected.<\/li>\n<li>Rendered file safety limits (apply to PNG and PDF):<\/li>\n<li><code>fileQuality: \"standard\"<\/code>: max 8 MP (8,000,000 rendered pixels).<\/li>\n<li><code>fileQuality: \"hq\"<\/code>: max 14 MP (14,000,000 rendered pixels).<\/li>\n<li><code>fileQuality: \"print\"<\/code>: max 24 MP (24,000,000 rendered pixels).<\/li>\n<li>PDF also has a max of 50 pages.<\/li>\n<\/ul>\n<h2>Output details contract<\/h2>\n<p>The tool returns structured metadata under <code>details<\/code>.<\/p>\n<p>Shared fields for modes that create a viewer:<\/p>\n<ul>\n<li><code>artifactId<\/code><\/li>\n<li><code>viewerUrl<\/code><\/li>\n<li><code>viewerPath<\/code><\/li>\n<li><code>title<\/code><\/li>\n<li><code>expiresAt<\/code><\/li>\n<li><code>inputKind<\/code><\/li>\n<li><code>fileCount<\/code><\/li>\n<li><code>mode<\/code><\/li>\n<li><code>context<\/code> (<code>agentId<\/code>, <code>sessionId<\/code>, <code>messageChannel<\/code>, <code>agentAccountId<\/code> when available)<\/li>\n<\/ul>\n<p>File fields when PNG or PDF is rendered:<\/p>\n<ul>\n<li><code>artifactId<\/code><\/li>\n<li><code>expiresAt<\/code><\/li>\n<li><code>filePath<\/code><\/li>\n<li><code>path<\/code> (same value as <code>filePath<\/code>, for message tool compatibility)<\/li>\n<li><code>fileBytes<\/code><\/li>\n<li><code>fileFormat<\/code><\/li>\n<li><code>fileQuality<\/code><\/li>\n<li><code>fileScale<\/code><\/li>\n<li><code>fileMaxWidth<\/code><\/li>\n<\/ul>\n<p>Mode behavior summary:<\/p>\n<ul>\n<li><code>mode: \"view\"<\/code>: viewer fields only.<\/li>\n<li><code>mode: \"file\"<\/code>: file fields only, no viewer artifact.<\/li>\n<li><code>mode: \"both\"<\/code>: viewer fields plus file fields. If file rendering fails, viewer still returns with <code>fileError<\/code>.<\/li>\n<\/ul>\n<h2>Collapsed unchanged sections<\/h2>\n<ul>\n<li>The viewer can show rows like <code>N unmodified lines<\/code>.<\/li>\n<li>Expand controls on those rows are conditional and not guaranteed for every input kind.<\/li>\n<li>Expand controls appear when the rendered diff has expandable context data, which is typical for before and after input.<\/li>\n<li>For many unified patch inputs, omitted context bodies are not available in the parsed patch hunks, so the row can appear without expand controls. This is expected behavior.<\/li>\n<li><code>expandUnchanged<\/code> applies only when expandable context exists.<\/li>\n<\/ul>\n<h2>Plugin defaults<\/h2>\n<p>Set plugin-wide defaults in <code>~\/.openclaw\/openclaw.json<\/code>:<\/p>\n<p>&#8220;`json5  theme={&#8220;theme&#8221;:{&#8220;light&#8221;:&#8221;min-light&#8221;,&#8221;dark&#8221;:&#8221;min-dark&#8221;}}<br \/>\n{<br \/>\n  plugins: {<br \/>\n    entries: {<br \/>\n      diffs: {<br \/>\n        enabled: true,<br \/>\n        config: {<br \/>\n          defaults: {<br \/>\n            fontFamily: &#8220;Fira Code&#8221;,<br \/>\n            fontSize: 15,<br \/>\n            lineSpacing: 1.6,<br \/>\n            layout: &#8220;unified&#8221;,<br \/>\n            showLineNumbers: true,<br \/>\n            diffIndicators: &#8220;bars&#8221;,<br \/>\n            wordWrap: true,<br \/>\n            background: true,<br \/>\n            theme: &#8220;dark&#8221;,<br \/>\n            fileFormat: &#8220;png&#8221;,<br \/>\n            fileQuality: &#8220;standard&#8221;,<br \/>\n            fileScale: 2,<br \/>\n            fileMaxWidth: 960,<br \/>\n            mode: &#8220;both&#8221;,<br \/>\n          },<br \/>\n        },<br \/>\n      },<br \/>\n    },<br \/>\n  },<br \/>\n}<\/p>\n<pre><code>\nSupported defaults:\n\n* `fontFamily`\n* `fontSize`\n* `lineSpacing`\n* `layout`\n* `showLineNumbers`\n* `diffIndicators`\n* `wordWrap`\n* `background`\n* `theme`\n* `fileFormat`\n* `fileQuality`\n* `fileScale`\n* `fileMaxWidth`\n* `mode`\n\nExplicit tool parameters override these defaults.\n\n## Security config\n\n* `security.allowRemoteViewer` (`boolean`, default `false`)\n  * `false`: non-loopback requests to viewer routes are denied.\n  * `true`: remote viewers are allowed if tokenized path is valid.\n\nExample:\n\n```json5  theme={&quot;theme&quot;:{&quot;light&quot;:&quot;min-light&quot;,&quot;dark&quot;:&quot;min-dark&quot;}}\n{\n  plugins: {\n    entries: {\n      diffs: {\n        enabled: true,\n        config: {\n          security: {\n            allowRemoteViewer: false,\n          },\n        },\n      },\n    },\n  },\n}\n<\/code><\/pre>\n<h2>Artifact lifecycle and storage<\/h2>\n<ul>\n<li>Artifacts are stored under the temp subfolder: <code>$TMPDIR\/openclaw-diffs<\/code>.<\/li>\n<li>Viewer artifact metadata contains:<\/li>\n<li>random artifact ID (20 hex chars)<\/li>\n<li>random token (48 hex chars)<\/li>\n<li><code>createdAt<\/code> and <code>expiresAt<\/code><\/li>\n<li>stored <code>viewer.html<\/code> path<\/li>\n<li>Default viewer TTL is 30 minutes when not specified.<\/li>\n<li>Maximum accepted viewer TTL is 6 hours.<\/li>\n<li>Cleanup runs opportunistically after artifact creation.<\/li>\n<li>Expired artifacts are deleted.<\/li>\n<li>Fallback cleanup removes stale folders older than 24 hours when metadata is missing.<\/li>\n<\/ul>\n<h2>Viewer URL and network behavior<\/h2>\n<p>Viewer route:<\/p>\n<ul>\n<li><code>\/plugins\/diffs\/view\/{artifactId}\/{token}<\/code><\/li>\n<\/ul>\n<p>Viewer assets:<\/p>\n<ul>\n<li><code>\/plugins\/diffs\/assets\/viewer.js<\/code><\/li>\n<li><code>\/plugins\/diffs\/assets\/viewer-runtime.js<\/code><\/li>\n<\/ul>\n<p>URL construction behavior:<\/p>\n<ul>\n<li>If <code>baseUrl<\/code> is provided, it is used after strict validation.<\/li>\n<li>Without <code>baseUrl<\/code>, viewer URL defaults to loopback <code>127.0.0.1<\/code>.<\/li>\n<li>If gateway bind mode is <code>custom<\/code> and <code>gateway.customBindHost<\/code> is set, that host is used.<\/li>\n<\/ul>\n<p><code>baseUrl<\/code> rules:<\/p>\n<ul>\n<li>Must be <code>http:\/\/<\/code> or <code>https:\/\/<\/code>.<\/li>\n<li>Query and hash are rejected.<\/li>\n<li>Origin plus optional base path is allowed.<\/li>\n<\/ul>\n<h2>Security model<\/h2>\n<p>Viewer hardening:<\/p>\n<ul>\n<li>Loopback-only by default.<\/li>\n<li>Tokenized viewer paths with strict ID and token validation.<\/li>\n<li>Viewer response CSP:<\/li>\n<li><code>default-src 'none'<\/code><\/li>\n<li>scripts and assets only from self<\/li>\n<li>no outbound <code>connect-src<\/code><\/li>\n<li>Remote miss throttling when remote access is enabled:<\/li>\n<li>40 failures per 60 seconds<\/li>\n<li>60 second lockout (<code>429 Too Many Requests<\/code>)<\/li>\n<\/ul>\n<p>File rendering hardening:<\/p>\n<ul>\n<li>Screenshot browser request routing is deny-by-default.<\/li>\n<li>Only local viewer assets from <code>http:\/\/127.0.0.1\/plugins\/diffs\/assets\/*<\/code> are allowed.<\/li>\n<li>External network requests are blocked.<\/li>\n<\/ul>\n<h2>Browser requirements for file mode<\/h2>\n<p><code>mode: \"file\"<\/code> and <code>mode: \"both\"<\/code> need a Chromium-compatible browser.<\/p>\n<p>Resolution order:<\/p>\n<ol>\n<li><code>browser.executablePath<\/code> in OpenClaw config.<\/li>\n<li>Environment variables:<br \/>\n   * <code>OPENCLAW_BROWSER_EXECUTABLE_PATH<\/code><br \/>\n   * <code>BROWSER_EXECUTABLE_PATH<\/code><br \/>\n   * <code>PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH<\/code><\/li>\n<li>Platform command\/path discovery fallback.<\/li>\n<\/ol>\n<p>Common failure text:<\/p>\n<ul>\n<li><code>Diff PNG\/PDF rendering requires a Chromium-compatible browser...<\/code><\/li>\n<\/ul>\n<p>Fix by installing Chrome, Chromium, Edge, or Brave, or setting one of the executable path options above.<\/p>\n<h2>Troubleshooting<\/h2>\n<p>Input validation errors:<\/p>\n<ul>\n<li><code>Provide patch or both before and after text.<\/code><\/li>\n<li>Include both <code>before<\/code> and <code>after<\/code>, or provide <code>patch<\/code>.<\/li>\n<li><code>Provide either patch or before\/after input, not both.<\/code><\/li>\n<li>Do not mix input modes.<\/li>\n<li><code>Invalid baseUrl: ...<\/code><\/li>\n<li>Use <code>http(s)<\/code> origin with optional path, no query\/hash.<\/li>\n<li><code>{field} exceeds maximum size (...)<\/code><\/li>\n<li>Reduce payload size.<\/li>\n<li>Large patch rejection<\/li>\n<li>Reduce patch file count or total lines.<\/li>\n<\/ul>\n<p>Viewer accessibility issues:<\/p>\n<ul>\n<li>Viewer URL resolves to <code>127.0.0.1<\/code> by default.<\/li>\n<li>For remote access scenarios, either:<\/li>\n<li>pass <code>baseUrl<\/code> per tool call, or<\/li>\n<li>use <code>gateway.bind=custom<\/code> and <code>gateway.customBindHost<\/code><\/li>\n<li>Enable <code>security.allowRemoteViewer<\/code> only when you intend external viewer access.<\/li>\n<\/ul>\n<p>Unmodified-lines row has no expand button:<\/p>\n<ul>\n<li>This can happen for patch input when the patch does not carry expandable context.<\/li>\n<li>This is expected and does not indicate a viewer failure.<\/li>\n<\/ul>\n<p>Artifact not found:<\/p>\n<ul>\n<li>Artifact expired due TTL.<\/li>\n<li>Token or path changed.<\/li>\n<li>Cleanup removed stale data.<\/li>\n<\/ul>\n<h2>Operational guidance<\/h2>\n<ul>\n<li>Prefer <code>mode: \"view\"<\/code> for local interactive reviews in canvas.<\/li>\n<li>Prefer <code>mode: \"file\"<\/code> for outbound chat channels that need an attachment.<\/li>\n<li>Keep <code>allowRemoteViewer<\/code> disabled unless your deployment requires remote viewer URLs.<\/li>\n<li>Set explicit short <code>ttlSeconds<\/code> for sensitive diffs.<\/li>\n<li>Avoid sending secrets in diff input when not required.<\/li>\n<li>If your channel compresses images aggressively (for example Telegram or WhatsApp), prefer PDF output (<code>fileFormat: \"pdf\"<\/code>).<\/li>\n<\/ul>\n<p>Diff rendering engine:<\/p>\n<ul>\n<li>Powered by <a href=\"https:\/\/diffs.com\">Diffs<\/a>.<\/li>\n<\/ul>\n<h2>Related docs<\/h2>\n<ul>\n<li><a href=\"\/tools\">Tools overview<\/a><\/li>\n<li><a href=\"\/tools\/plugin\">Plugins<\/a><\/li>\n<li><a href=\"\/tools\/browser\">Browser<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Diffs diffs is an optional plugin tool with short built [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-642","post","type-post","status-publish","format-standard","hentry","category-docs"],"_links":{"self":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/642","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/comments?post=642"}],"version-history":[{"count":2,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/642\/revisions"}],"predecessor-version":[{"id":702,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/posts\/642\/revisions\/702"}],"wp:attachment":[{"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/media?parent=642"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/categories?post=642"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/pa.yingzhi8.cn\/index.php\/wp-json\/wp\/v2\/tags?post=642"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}