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 with after when patch is not provided.

  • after (string), The updated text. Required with before when patch is not provided.

  • patch (string), Unified diff text. Cannot be used together with before and after.

  • 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 default defaults.mode (both). The deprecated alias "image" behaves exactly like "file".

  • theme (light" | "dark), Viewer theme. Defaults to the plugin default defaults.theme.

  • layout (unified" | "split), Diff layout. Defaults to the plugin default defaults.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 default defaults.fileFormat.

  • fileQuality (standard" | "hq" | "print), Quality preset for PNG/PDF rendering.

  • fileScale (number), Device scale override (range 1 to 4).

  • fileMaxWidth (number), Maximum render width in CSS pixels (range 640 to 2400).

  • ttlSeconds (numeric, default: 1800), Determines how long artifacts remain available, in seconds, for both the viewer and standalone file outputs. The maximum allowed value is 21600.

  • baseUrl (text), Sets a custom origin for the viewer URL. This replaces the plugin's default viewerBaseUrl. It must be either http or https and cannot include query parameters or a hash fragment.

Validation and limits

  • before and after: 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 patch with before or after is 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)

  • changed
  • artifactId
  • viewerUrl
  • viewerPath
  • title
  • expiresAt
  • inputKind
  • fileCount
  • mode
  • context (agentId, sessionId, messageChannel, agentAccountId when available)

File fields (file and both modes)

  • changed
  • artifactId
  • expiresAt
  • filePath
  • path (identical to filePath, for message tool compatibility)
  • fileBytes
  • fileFormat
  • fileQuality
  • fileScale
  • fileMaxWidth
ModeReturns
"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 when baseUrl is not supplied in a tool call. Must be http or https; 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), When false is set, non-loopback requests to viewer routes are rejected. With true enabled, 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.sqlite database 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-diffs because 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, and viewer.html are 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 outbound connect-src is 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_PATH
  • BROWSER_EXECUTABLE_PATH
  • PLAYWRIGHT_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 both before and after, or give patch.
  • Provide either patch or before/after input, not both. -- avoid mixing different input types.
  • Invalid baseUrl: ... -- use an http(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, provide baseUrl with each call, or use gateway.bind=custom together with gateway.customBindHost.
  • When gateway.trustedProxies contains 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 enable security.allowRemoteViewer along with plugin viewerBaseUrl or a proxy baseUrl to create a shareable viewer link.
  • Only turn on security.allowRemoteViewer when 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 allowRemoteViewer turned 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.