Skip to content

doctor: copy-sensitive paths get re-wrapped by clack note box, splitting file extensions #94730

Description

@berkgungor

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

openclaw doctor mangles long file paths inside note(...) boxes by re-wrapping copy-sensitive tokens (paths, URLs) at the box width even though wrapNoteMessage correctly preserved them as a single unbroken token. The mangled line cannot be copy-pasted, which defeats the entire reason the path is being surfaced.

Steps to reproduce

  1. Create a real or fake session lock so that Session locks has something to report. Easy fake:
    mkdir -p ~/.openclaw/agents/main/sessions
    touch ~/.openclaw/agents/main/sessions/9c2acae5-841f-4aea-936b-fdb513b60202.jsonl.lock
  2. Run openclaw doctor in an 80-column terminal (default in many CI / Docker / Pi setups).
  3. Observe the Session locks panel — the lock path is split mid-extension.

Or, isolated repro that does not require a running gateway — exercises the exact same @clack/prompts.note() path used by src/commands/doctor-session-locks.ts:

mkdir /tmp/note-wrap-repro && cd /tmp/note-wrap-repro
npm init -y >/dev/null && npm install @clack/prompts >/dev/null
COLUMNS=80 node -e "
process.stdout.columns = 80;
import('@clack/prompts').then(m => {
  m.note(
    '- Found 1 session lock file.\n' +
    '- ~/.openclaw/agents/main/sessions/9c2acae5-841f-4aea-936b-fdb513b60202.jsonl.lock pid=86519 (alive) age=2m47s stale=no',
    'Session locks'
  );
});
"

Expected

The lock file path is rendered as a single unbroken token so the user can:

  • Visually identify the lock file
  • Triple-click / copy-paste it into a follow-up ls, cat, rm, or lsof command

Either the path stays on its own line (overflowing the box if needed), or the box widens to fit it, or it gets a clearly-marked continuation glyph (e.g. trailing ) so the reader knows the break is cosmetic. Splitting .jsonl.lock into .js + onl.lock is the worst outcome.

Actual

◇  Session locks ──────────────────────────────────────────────────────────────╮
│                                                                              │
│  - Found 1 session lock file.                                                │
│  - ~/.openclaw/agents/main/sessions/9c2acae5-841f-4aea-936b-fdb513b60202.js  │
│  onl.lock pid=86519 (alive) age=2m47s stale=no                               │
│                                                                              │
├──────────────────────────────────────────────────────────────────────────────╯

Note .js at the end of one line and onl.lock at the start of the next. Reproduced verbatim with openclaw@2026.4.15 against my real gateway, and against bare @clack/prompts@<latest> in an isolated /tmp directory (no OpenClaw runtime involved).

Affected versions

  • Locally observed on openclaw@2026.4.15 (041266a).
  • Source still vulnerable on main (HEAD c33cec04d9): packages/terminal-core/src/note.ts calls clackNote(...) after wrapping, and clackNote does its own width-based wrapping inside the box.

Root cause (source-level)

packages/terminal-core/src/note.ts correctly handles copy-sensitive tokens in its own wrapLine function — isCopySensitiveToken (line 37) returns true for words starting with ~/, /, ./, ../, URLs, Windows drives, etc., and pushWrappedWordSegments is skipped for those tokens at lines 99–101 and 129–131. The token is allowed to overflow the chosen maxWidth rather than be broken mid-word.

The bug is that wrapNoteMessage is only the first pass. The function then calls clackNote(message, title) (line ~205), which is @clack/prompts.note. Clack performs its own width-based wrapping for the bordered box and is unaware of isCopySensitiveToken semantics — it just word-breaks/character-breaks at the box width. With the default MIN_NOTE_COLUMNS = 80, the 80-character path lands exactly at the box's right edge and gets broken inside .jsonl.lock.

The unit-test file packages/terminal-core/src/note.test.ts does not exist; there is no regression test covering the interaction between wrapNoteMessage and the downstream clackNote render. (Adjacent files in packages/terminal-core/src/*.test.ts do exist for ansi, links, table, etc., so adding one is in-keeping with the package convention.)

Suggested fix

A few options, ranked rough-best to rough-worst by effort × impact:

  1. Special-case copy-sensitive lines. Before handing the message to clackNote, detect lines whose only oversized token is copy-sensitive and emit them on their own line inside the box (i.e. let them overflow the box right edge, or render them outside the box prefix/suffix). This matches what wrapNoteMessage was already trying to do.

  2. Stop relying on clack's box wrap. Build the bordered box in packages/terminal-core/src/note.ts directly so OpenClaw owns the wrapping decisions end-to-end. The bordered output is already largely OpenClaw-controlled (the title, the /// glyphs, the column resolution) — this would be a continuation of that direction.

  3. Shorten the path proactively. For known patterns (session lock files), use a middle-truncation that fits the box, and print the full path separately when --verbose is on. Lossy and arguably worse — the path is supposed to be actionable — but minimally invasive.

I think option 1 is the smallest change that preserves the user-facing contract (isCopySensitiveToken already exists and is the right concept to extend).

Notes

  • 🦞 Diamond-lobster shape: source-reproducible (file + line), isolated repro without the gateway, narrow blast radius (one rendering helper, one downstream clackNote call).
  • Same code path is used by other doctor sections that print paths (note() is used in src/commands/doctor-state-integrity.ts, doctor-volatile-fs.ts, etc.), so this fix benefits multiple panels at once.
  • AI-assisted issue (Claude). Repro verified end-to-end against both my installed openclaw doctor and bare @clack/prompts in /tmp.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.no-staleExclude from stale automation

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions