Diffs Tool: Read-Only Diff Viewer and File Renderer for Agents
Learn how to use the optional diffs plugin tool to convert before/after text or a unified patch into a read-only diff artifact. This page covers installation, modes, and system guidance for agents.
Read this when
- You want agents to show code or markdown edits as diffs
- You want a canvas-ready viewer URL or a rendered diff file
- You need controlled, temporary diff artifacts with secure defaults
diffs is an optional bundled plugin tool that converts before/after text or a unified patch into a read-only diff artifact. Additionally, it inserts brief agent guidance into the system prompt and includes a companion skill for more detailed instructions.
Input: before and after text, or a unified patch (these options are mutually exclusive).
Output: a gateway viewer URL for canvas display, a rendered PNG/PDF file path for message delivery, or both.
Quick start
Install the plugin
openclaw plugins install diffs
Enable the plugin
{
plugins: {
entries: {
diffs: {
enabled: true,
},
},
},
}
Pick a mode
view
For canvas-first workflows, agents call diffs with mode: "view" and open details.viewerUrl with canvas present.
file
For chat file delivery, agents call diffs with mode: "file" and send details.filePath with message using path or filePath.
both
Combined mode (the default): agents call diffs with mode: "both" to obtain both artifacts in a single call.
Disable built-in system guidance
To retain the tool while removing the prepended system prompt guidance, set plugins.entries.diffs.hooks.allowPromptInjection to false:
{
plugins: {
entries: {
diffs: {
enabled: true,
hooks: {
allowPromptInjection: false,
},
},
},
},
}
This prevents the plugin's before_prompt_build hook from firing while still keeping the tool and skill accessible. To disable both the guidance and the tool, disable the plugin entirely.
Tool input reference
All fields are optional unless indicated otherwise.
-
before(string), The original text. Required withafterwhenpatchis not provided. -
after(string), The updated text. Required withbeforewhenpatchis not provided. -
patch(string), Unified diff text. Cannot be used together withbeforeandafter. -
path(string), Display filename used in before/after mode. -
lang(string), Language override hint for before/after mode. Unrecognized values and languages outside the default viewer set revert to plain text unless the Diff Viewer Language Pack plugin is installed. -
title(string), Override for the viewer title. -
mode(view" | "file" | "both), Output mode. Defaults to the plugin defaultdefaults.mode(both). The deprecated alias"image"behaves exactly like"file". -
theme(light" | "dark), Viewer theme. Defaults to the plugin defaultdefaults.theme. -
layout(unified" | "split), Diff layout. Defaults to the plugin defaultdefaults.layout. -
expandUnchanged(boolean), Expands unchanged sections when full context is available. This option is per call only and not a plugin default key. -
fileFormat(png" | "pdf), Rendered file format. Defaults to the plugin defaultdefaults.fileFormat. -
fileQuality(standard" | "hq" | "print), Quality preset for PNG/PDF rendering. -
fileScale(number), Device scale override (range1to4). -
fileMaxWidth(number), Maximum render width in CSS pixels (range640to2400). -
ttlSeconds(numeric, default: 1800), Determines how long artifacts remain available, in seconds, for both the viewer and standalone file outputs. The maximum allowed value is21600. -
baseUrl(text), Sets a custom origin for the viewer URL. This replaces the plugin's defaultviewerBaseUrl. It must be eitherhttporhttpsand cannot include query parameters or a hash fragment.
Validation and limits
beforeandafter: each limited to 512 KiB.patch: limited to 2 MiB.path: limited to 2048 bytes.lang: limited to 128 bytes.title: limited to 1024 bytes.- Patch complexity: no more than 128 files and 120,000 total lines.
- Combining
patchwithbeforeorafteris not permitted. - Safety limits for rendered files (PNG and PDF):
fileQuality: "standard": maximum 8 MP (8,000,000 rendered pixels).fileQuality: "hq": maximum 14 MP.fileQuality: "print": maximum 24 MP.- PDF output is also capped at 50 pages.
Syntax highlighting
Languages included by default:
javascript, typescript, tsx, jsx, json, markdown, yaml, css, html, sh, python, go, rust, java, c, cpp, csharp, php, sql, docker, ruby, swift, kotlin, r, dart, lua, powershell, xml, and toml.
Common aliases (such as js, ts, bash, md, yml, c++, dockerfile, rb, kt, ps1, and others) are automatically mapped to these languages.
To add support for additional languages (Astro, Vue, Svelte, MDX, GraphQL, Terraform/HCL, Nix, Clojure, Elixir, Haskell, OCaml, Scala, Zig, Solidity, Verilog/VHDL, Fortran, MATLAB, LaTeX, Mermaid, Sass/Less/SCSS, Nginx, Apache, CSV, dotenv, INI, diff, and more), install the Diff Viewer Language Pack plugin:
openclaw plugins install clawhub:@openclaw/diffs-language-pack
Without the pack installed, unsupported languages still appear as legible plain text. For the upstream catalog, refer to the Diffs Language Pack plugin and Shiki languages.
Output details contract
Every successful outcome includes changed: when the before and after input are identical, false is returned without generating an artifact; rendered results return true.
Viewer fields (view and both modes)
changedartifactIdviewerUrlviewerPathtitleexpiresAtinputKindfileCountmodecontext(agentId,sessionId,messageChannel,agentAccountIdwhen available)
File fields (file and both modes)
changedartifactIdexpiresAtfilePathpath(identical tofilePath, for message tool compatibility)fileBytesfileFormatfileQualityfileScalefileMaxWidth
| Mode | Returns |
|---|---|
"view" | Only viewer fields. |
"file" | Only file fields, no viewer artifact. |
"both" | Viewer fields and file fields. If file rendering fails, the viewer still returns with fileError. |
Collapsed unchanged sections
The viewer displays rows like N unmodified lines. Expand controls show up only when the rendered diff includes expandable context data, which is typical for before/after input. In many unified patches, context bodies are omitted from hunks, so a row may appear without an expand control. This is expected, not a defect. expandUnchanged applies only when expandable context is present.
Multi-file navigation
Patches that modify more than one file begin with a summary card for changed files: total +N / -N counts, per-file counts, added/deleted/renamed badges, and anchor links that jump to each file. Rendered PNG/PDF files keep the per-file header counts but remove the interactive view toggles, since those controls are useless in a static file.
Plugin defaults
Set plugin-wide defaults in ~/.openclaw/openclaw.json:
{
plugins: {
entries: {
diffs: {
enabled: true,
config: {
defaults: {
fontFamily: "Fira Code",
fontSize: 15,
lineSpacing: 1.6,
layout: "unified",
showLineNumbers: true,
diffIndicators: "bars",
wordWrap: true,
background: true,
theme: "dark",
fileFormat: "png",
fileQuality: "standard",
fileScale: 2,
fileMaxWidth: 960,
mode: "both",
ttlSeconds: 21600,
},
},
},
},
},
}
Supported defaults keys include fontFamily, fontSize, lineSpacing, layout, showLineNumbers, diffIndicators, wordWrap, background, theme, fileFormat, fileQuality, fileScale, fileMaxWidth, mode, and ttlSeconds. Values passed directly to a tool call take precedence over these.
Persistent viewer URL config
viewerBaseUrl(string), A plugin-level fallback for viewer links returned whenbaseUrlis not supplied in a tool call. Must behttporhttps; query strings and fragments are not allowed.
{
plugins: {
entries: {
diffs: {
enabled: true,
config: {
viewerBaseUrl: "https://gateway.example.com/openclaw",
},
},
},
},
}
Security config
security.allowRemoteViewer(boolean, default: false), Whenfalseis set, non-loopback requests to viewer routes are rejected. Withtrueenabled, remote viewers are permitted provided the tokenized path passes validation.
{
plugins: {
entries: {
diffs: {
enabled: true,
config: {
security: {
allowRemoteViewer: false,
},
},
},
},
},
}
Artifact lifecycle and storage
- Viewer HTML and its metadata are stored in the shared
state/openclaw.sqlitedatabase within the Diffs plugin blob namespace. HTML content is gzip-compressed; only a SHA-256 hash of the random URL token is kept in SQLite, never the token itself. - Rendered PNG and PDF files are stored as temporary materializations under
$TMPDIR/openclaw-diffsbecause a file path is required for channel delivery. Expiry metadata for these files is managed by SQLite, with no JSON sidecars written. - Default artifact TTL is 30 minutes. The maximum allowed TTL is 6 hours.
- Cleanup runs opportunistically after each artifact creation call. Expired rows in SQLite are removed first, followed by any associated PNG or PDF directories.
- A fallback sweep deletes temporary folders without corresponding rows that are older than 24 hours. Legacy caches from
meta.json,file-meta.json, andviewer.htmlare neither imported nor read.
Viewer URL and network behavior
Viewer route: /plugins/diffs/view/{artifactId}/{token}
Viewer assets:
/plugins/diffs/assets/viewer.js/plugins/diffs/assets/viewer-runtime.js/plugins/diffs-language-pack/assets/viewer.js(only when the diff uses a language pack language)
The viewer document resolves these assets relative to the viewer URL, so an optional baseUrl path prefix is also carried through to asset requests.
URL resolution order: tool-call baseUrl (after strict validation) -> plugin viewerBaseUrl -> loopback 127.0.0.1 default. When gateway bind mode is custom and gateway.customBindHost is configured, that host is used instead of the loopback address.
baseUrl rules: must be http:// or https://; query strings and hashes are rejected; an origin plus an optional base path is acceptable.
Security model
Viewer hardening
- Loopback access only by default.
- Tokenized viewer paths enforce strict validation of IDs and token patterns.
- Viewer response CSP:
default-src 'none'; scripts and assets are restricted to self; no outboundconnect-srcis allowed. - When remote access is enabled, remote miss throttling applies: 40 failures within 60 seconds triggers a 60-second lockout (
429 Too Many Requests).
File rendering hardening
- Screenshot browser request routing denies all traffic by default.
- Only local viewer assets from
http://127.0.0.1/plugins/diffs/assets/*are permitted. - All external network requests are blocked.
Browser requirements for file mode
mode: "file" and mode: "both" require a Chromium-compatible browser.
Resolution order:
Config
browser.executablePath in OpenClaw config.
Environment variables
OPENCLAW_BROWSER_EXECUTABLE_PATHBROWSER_EXECUTABLE_PATHPLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
Platform fallback
Locations where Chrome, Chromium, Edge, and Brave are typically installed, plus PATH lookup paths.
Error message commonly shown: Diff PNG/PDF rendering requires a Chromium-compatible browser.... To resolve this, install Chrome, Chromium, Edge, or Brave, or configure one of the executable path settings described above.
Troubleshooting
Input validation errors
Provide patch or both before and after text.-- supply bothbeforeandafter, or givepatch.Provide either patch or before/after input, not both.-- avoid mixing different input types.Invalid baseUrl: ...-- use anhttp(s)origin, optionally with a path, but no query string or fragment.{field} exceeds maximum size (...)-- keep the payload small.- Large patch rejection -- lower the number of patch files or total lines.
Viewer accessibility
- The viewer URL defaults to
127.0.0.1. - For remote access, configure the plugin
viewerBaseUrl, providebaseUrlwith each call, or usegateway.bind=customtogether withgateway.customBindHost. - When
gateway.trustedProxiescontains a loopback address for a same-host proxy (such as Tailscale Serve), raw loopback viewer requests that lack forwarded client IP headers are intentionally rejected. - For that proxy setup, use
mode: "file"/"both"for an attachment, or explicitly enablesecurity.allowRemoteVieweralong with pluginviewerBaseUrlor a proxybaseUrlto create a shareable viewer link. - Only turn on
security.allowRemoteViewerwhen external viewer access is needed.
Unmodified-lines row has no expand button
This is expected when patch input has no expandable context; it does not indicate a viewer problem.
Artifact not found
- The artifact expired based on its TTL.
- The token or path was modified.
- Cleanup removed outdated data.
Operational guidance
- For local interactive reviews in the canvas, use
mode: "view". - For outbound chat channels that require an attachment, use
mode: "file". - Leave
allowRemoteViewerturned off unless your setup needs remote viewer URLs. - For sensitive diffs, set an explicit short
ttlSeconds. - Do not send secrets in diff input unless necessary.
- If your channel compresses images heavily (for example Telegram or WhatsApp), output as PDF using
fileFormat: "pdf".
Note
The diff rendering engine is powered by Diffs.