# Clavue v8.13.2 — Outbound Image Token Preflight (one gate to rule them all)

Released: 2026-05-19

## Why this exists

v8.13.0 and v8.13.1 added a per-image token budget at individual entry
points (clipboard, drag-in, PDF, bash stdout, MCP, FileRead, REPL remote,
permission prompts). The per-entry-point approach has a fundamental
problem: **every new image-producing code path needs its own guard**, and
two of them (Jupyter notebook outputs, computer-use screenshots) are
still uncovered. Adding more sites is not a fix — it is a treadmill.

v8.13.2 moves the invariant down to the egress layer: the **last gate
before any image leaves for the provider** is a single function that
walks every outbound message and enforces `CLAVUE_IMAGE_MAX_TOKENS`
across all image blocks, including those nested inside `tool_result`
content.

This makes the per-entry-point guards belt-and-suspenders. They still
reduce wasted bytes on the wire, but the egress invariant is what
guarantees that no image block ever exceeds the budget — regardless of
which code path produced it, and regardless of whether that code path
remembered to call the helper.

## Architecture

```
                       [paste]   [drag]   [PDF]    [bash]
                       [MCP]     [FileRead]        [permission]
                       [REPL]    [notebook?]       [computer-use?]
                          \         |         /
                           \   per-entry guards (best-effort, optional)
                            \      |        /
                             v     v       v
                          messagesForAPI normalization
                                  |
                                  v
                       stripExcessMediaItems (100-item cap)
                                  |
                                  v
                       enforceOutboundImageTokenBudget  <-- 8.13.2
                                  |  (single invariant)
                                  v
                            provider.send(...)
```

The egress gate lives in `src/services/api/outboundImageGuard.ts` and
wires the budget-applying core (`outboundImageGuardCore.ts`) to the
production deps (`getImagePasteTokenBudget`, `applyTokenBudgetToImage`,
`logEvent`). Splitting the core out makes it trivially unit-testable
without dragging in sharp / analytics / config.

The hook into the egress pipeline is one line in
`src/services/api/claude.ts`, right after `stripExcessMediaItems`.

## Properties

- **Idempotent.** Images already within budget take a cheap O(1)
  pre-check (`estimateImageBufferTokens` on base64 length) and pass
  through by reference. Running the guard twice on the same payload
  costs the same as running it once.
- **Coverage by construction.** Any future code path that puts an image
  block into a `UserMessage` / `AssistantMessage` is automatically
  guarded. The Jupyter notebook image path and the computer-use
  screenshot path — both of which were unguarded in 8.13.1 — are now
  covered without touching their source files.
- **Cache-safe.** When no image needs trimming, per-message references
  are preserved, so prompt-cache breakpoints don't move.
- **Failure-safe.** When the recompress hook returns null (e.g. sharp
  failure), the original block is preserved and the runtime PTL
  recovery path takes over downstream.
- **Disable-able.** `CLAVUE_IMAGE_MAX_TOKENS=0` short-circuits the
  guard before any per-block work.

## Telemetry

When the guard trims at least one image, it emits
`tengu_outbound_image_token_guard` with `{ scanned, trimmed }` so we can
see how often the egress invariant catches images that slipped past the
per-entry-point guards.

## Coverage matrix (cumulative)

| Entry point | 8.13.0 | 8.13.1 | 8.13.2 |
|---|---|---|---|
| Clipboard paste / drag-in | ✅ | ✅ | ✅ |
| Pasted attachments | ✅ | ✅ | ✅ |
| PDF page extraction | ✅ | ✅ | ✅ |
| Bash stdout image | ✅ | ✅ | ✅ |
| SDK / bridge inbound image | ✅ | ✅ | ✅ |
| Permission prompt attachments | ✅ | ✅ | ✅ |
| FileReadTool single image | — | ✅ | ✅ |
| MCP tool result image | — | ✅ | ✅ |
| MCP resource blob image | — | ✅ | ✅ |
| REPL remote-send pastedContents | — | ✅ | ✅ |
| **Jupyter notebook output image** | — | — | ✅ (egress) |
| **Computer-use screenshot** | — | — | ✅ (egress) |
| **Any future image entry point** | — | — | ✅ (egress) |

## Verification

- `tests/outbound-image-token-guard.test.mjs`: 9 unit tests covering
  identity, recompression, budget-zero, nested `tool_result`, null
  recompress, telemetry, non-base64 sources, idempotency.
- Full suite: 1525 tests / 1519 pass / 6 skipped / 0 fail (≈62s).
- `dist/cli.js --version` → `8.13.2 (Clavue)`.

## Configuration

Unchanged from v8.13.0:

| Env | Default | Effect |
|---|---|---|
| `CLAVUE_IMAGE_MAX_TOKENS` | `1500` | Per-image input token cap |
| `CLAVUE_IMAGE_MAX_TOKENS=0` | — | Disable token guard (byte-only resize) |
