diffs is an optional plugin tool that turns before/after text or a unified patch into a read-only diff artifact. It also prepends short agent guidance into the system prompt and ships a companion skill for fuller instructions.

Input: before + after text, or a unified patch (mutually exclusive).

Output: a gateway viewer URL for browser presentation, a rendered PNG/PDF file path for message delivery, or both.

The Control UI already highlights inline tool diffs and session diffs without this plugin. Install diffs when agents need standalone viewer links or rendered attachments for other channels.

Quick start

Install the plugin

```bash
apus plugins install clawhub:@openclaw/diffs
```

Installation applies to a running Gateway automatically; otherwise it takes effect
on the next startup. See [Apply changes and inspect](/plugins/manage-plugins#apply-changes-and-inspect).

`diffs` and its language pack ship as separate packages rather than with
Apus, so the install needs a scoped locator. The `clawhub:` prefix
selects the ClawHub copy of `@openclaw/diffs`. Use
`npm:@openclaw/diffs` to install from npm instead.

Enable the plugin

```json5
{
  plugins: {
    entries: {
      diffs: {
        enabled: true,
      },
    },
  },
}
```

Pick a mode

view

    Browser flows: agents call `diffs` with `mode: "view"` and open `details.viewerUrl` in a browser.

file

    Chat file delivery: agents call `diffs` with `mode: "file"` and send `details.filePath` with `message` using `path` or `filePath`.

both

    Combined (default): agents call `diffs` with `mode: "both"` to get both artifacts in one call.

Disable built-in system guidance

To keep the tool but drop the prepended system-prompt guidance, set plugins.entries.diffs.hooks.allowPromptInjection to false:

{
  plugins: {
    entries: {
      diffs: {
        enabled: true,
        hooks: {
          allowPromptInjection: false,
        },
      },
    },
  },
}

This blocks the plugin's before_prompt_build hook while keeping the tool and skill available. To disable both guidance and the tool, disable the plugin instead.

Tool input reference

All fields are optional unless noted.

before string

Original text. Required with after when patch is omitted.

after string

Updated text. Required with before when patch is omitted.

patch string

Unified diff text. Mutually exclusive with before and after.

path string

Display filename for before/after mode.

lang string

Language override hint for before/after mode. Unknown values and languages outside the default viewer set fall back to plain text unless the Diffs Language Pack plugin is installed.

title string

Viewer title override.

mode "view" | "file" | "both"

Output mode. Defaults to plugin default defaults.mode (both). Deprecated alias: "image" behaves identically to "file".

theme "light" | "dark"

Viewer theme. Defaults to plugin default defaults.theme.

layout "unified" | "split"

Diff layout. Defaults to plugin default defaults.layout.

expandUnchanged boolean

Expand unchanged sections when full context is available. Per-call option only (not a plugin default key).

fileFormat "png" | "pdf"

Rendered file format. Defaults to plugin default defaults.fileFormat.

fileQuality "standard" | "hq" | "print"

Quality preset for PNG/PDF rendering.

fileScale number

Device scale override (1-4).

fileMaxWidth number

Max render width in CSS pixels (640-2400).

ttlSeconds number

Artifact TTL in seconds for viewer and standalone file outputs. Max 21600.

baseUrl string

Viewer URL origin override. Overrides plugin viewerBaseUrl. Must be http or https, no query/hash.

Validation and limits
- `before`/`after`: max 512 KiB each.
- `patch`: max 2 MiB.
- `path`: max 2048 bytes.
- `lang`: max 128 bytes.
- `title`: max 1024 bytes.
- Patch complexity cap: max 128 files and 120000 total lines.
- `patch` together with `before`/`after` is rejected.
- Rendered file safety limits (PNG and PDF):
  - `fileQuality: "standard"`: max 8 MP (8,000,000 rendered pixels).
  - `fileQuality: "hq"`: max 14 MP.
  - `fileQuality: "print"`: max 24 MP.
  - PDF also caps at 50 pages.

Syntax highlighting

Built-in languages:

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 (js, ts, bash, md, yml, c++, dockerfile, rb, kt, ps1, etc.) normalize to those languages.

Install the Diffs Language Pack plugin for more 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):

apus plugins install clawhub:@openclaw/diffs-language-pack

Without the pack, unsupported languages still render as readable plain text. See Diffs Language Pack plugin and Shiki languages for the upstream catalog.

Output details contract

All successful results include changed: identical before/after input returns false without creating 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` (same value as `filePath`, for message tool compatibility)
- `fileBytes`
- `fileFormat`
- `fileQuality`
- `fileScale`
- `fileMaxWidth`
Mode Returns
"view" Viewer fields only.
"file" File fields only, no viewer artifact.
"both" Viewer fields plus file fields. If file rendering fails, viewer still returns with fileError.

Collapsed unchanged sections

The viewer shows rows like N unmodified lines. Expand controls only appear when the rendered diff has expandable context data (typical for before/after input). Many unified patches omit context bodies in their hunks. The row can then appear without an expand control. That is expected, not a bug. expandUnchanged only applies when expandable context exists.

Multi-file navigation

Patches that touch more than one file start with a changed-files summary card. The card shows:

Rendered PNG/PDF files keep the per-file header counts. They drop the interactive view toggles, because those are dead controls in a static file.

Plugin defaults

Set plugin-wide defaults in ~/.apus/apus.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: fontFamily, fontSize, lineSpacing, layout, showLineNumbers, diffIndicators, wordWrap, background, theme, fileFormat, fileQuality, fileScale, fileMaxWidth, mode, ttlSeconds. Explicit tool call parameters override these.

Persistent viewer URL config

viewerBaseUrl string

Plugin-owned fallback for returned viewer links when a tool call does not pass baseUrl. Must be http or https, no query/hash.

{
  plugins: {
    entries: {
      diffs: {
        enabled: true,
        config: {
          viewerBaseUrl: "https://gateway.example.com/apus",
        },
      },
    },
  },
}

Security config

security.allowRemoteViewer boolean

false: non-loopback requests to viewer routes are denied. true: remote viewers are allowed if the tokenized path is valid.

{
  plugins: {
    entries: {
      diffs: {
        enabled: true,
        config: {
          security: {
            allowRemoteViewer: false,
          },
        },
      },
    },
  },
}

Artifact lifecycle and storage

Viewer URL and network behavior

Viewer route: /plugins/diffs/view/{artifactId}/{token}

Viewer assets:

The viewer document resolves these assets relative to the viewer URL. An optional baseUrl path prefix therefore carries through to asset requests too.

URL resolution order: tool-call baseUrl (after strict validation) -> plugin viewerBaseUrl -> gateway.publicOrigin -> the existing bind-aware Gateway fallback.

baseUrl rules:

Security model

Viewer hardening
- Loopback-only by default.
- Tokenized viewer paths with strict ID and token pattern validation.
- Viewer response CSP: `default-src 'none'`. Scripts and assets load only from self. The viewer makes no outbound `connect-src` requests.
- Remote miss throttling when remote access is enabled: 40 failures per 60 seconds triggers a 60-second lockout (`429 Too Many Requests`).
File rendering hardening
- Screenshot browser request routing is deny-by-default.
- Only local viewer assets from `http://127.0.0.1/plugins/diffs/assets/*` are allowed.
- External network requests are blocked.

Browser requirements for file mode

mode: "file" and mode: "both" need a Chromium-compatible browser.

Resolution order:

Config

`browser.executablePath` in Apus config.

Environment variables

- `APUS_BROWSER_EXECUTABLE_PATH`
- `BROWSER_EXECUTABLE_PATH`
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`

Platform fallback

Common install paths and `PATH` lookups for Chrome, Chromium, Edge, and Brave.

Common failure text: Diff PNG/PDF rendering requires a Chromium-compatible browser.... Fix by installing Chrome, Chromium, Edge, or Brave, or setting one of the executable path options above.

Troubleshooting

Input validation errors
- `Provide patch or both before and after text.` -- include both `before` and `after`, or provide `patch`.
- `Provide either patch or before/after input, not both.` -- do not mix input modes.
- `Invalid baseUrl: ...` -- use an `http(s)` origin with optional path, no query/hash.
- `{field} exceeds maximum size (...)` -- reduce payload size.
- Large patch rejection -- reduce patch file count or total lines.
Viewer accessibility
- Viewer URL resolves to `127.0.0.1` by default.
- For remote access, set `gateway.publicOrigin`, set plugin `viewerBaseUrl`, or pass `baseUrl` per call.
- `gateway.trustedProxies` can include loopback for a same-host proxy such as Tailscale Serve. Raw loopback viewer requests without forwarded client-IP headers then fail closed by design.
- For that proxy topology, prefer `mode: "file"`/`"both"` for an attachment. For a shareable viewer link, intentionally enable `security.allowRemoteViewer` plus plugin `viewerBaseUrl`/a proxy `baseUrl`.
- Enable `security.allowRemoteViewer` only when external viewer access is intended.
Unmodified-lines row has no expand button
Expected for patch input that lacks expandable context. This is not a viewer failure.
Artifact not found
- Artifact expired due to TTL.
- Token or path changed.
- Cleanup removed stale data.

Operational guidance

Diff rendering engine powered by Diffs.

Related