Release v0.51.558 — escape-target symlinks display-only (#4581)#4610
Conversation
…as display-only workspace rows claw-io: symlinks whose target resolves outside the workspace root were silently dropped; now emitted with target_outside_workspace=True (display-only — safe_resolve_ws/open_anchored_fd still block navigation). Dedup-winner over #4573 (stronger tests). Co-authored-by: claw-io <claw-io@users.noreply.github.com>
…path/metadata
Codex+Opus gate (info-leak): the display-only escape-target rows still returned the resolved
outside path (target), target-derived is_dir, and target stat size; ui.js showed the outside path
in the open-dialog. Fix: emit ONLY name/path/type/target_outside_workspace/mtime for outside rows
(no target, no size, is_dir=False); dialog uses a generic 'points outside workspace' message (dropped
{target} from external_link_open_confirm across all 13 locales). Containment was already intact (both
gates confirmed); this closes the path-disclosure. Tests updated to assert the no-leak property.
…hideCancel confirm
Codex gate (SILENT): the new outside-symlink info dialog uses showConfirmDialog({hideCancel:true})
which sets the shared #appDialogCancel to display:none; showPromptDialog only reset textContent, so a
later rename/new-file prompt opened with no Cancel button. Fix: showPromptDialog now restores
cancelBtn.style.display='' first. + regression test.
…ows (#4581) + dialog Cancel fix
|
| Filename | Overview |
|---|---|
| api/workspace.py | External symlinks are now emitted with target_outside_workspace=True instead of being silently dropped; containment gates are unchanged, _is_blocked_system_path still fully filters system paths, and the entry intentionally omits target/real is_dir/size to prevent info-leak. |
| static/ui.js | Adds isExternalLink classification, display-only row rendering, info-dialog on click, and the hideCancel/restore-cancel-button dialog fix; the name-span tooltip branch doesn't guard isExternalLink, leaving 'Double-click to rename' shown on rows that open an info dialog on double-click. |
| static/i18n.js | Adds external_link_open_confirm to all 13 locale blocks; strings are English-only (consistent with other recently-added strings in the file). |
| static/icons.js | Adds the external-link SVG path to LI_PATHS; the path data is standard feather-style and referenced correctly by the new renderer branch. |
| tests/test_workspace_symlink_containment.py | Updates containment assertions to reflect display-only emission; read/traversal blocking assertions are preserved and still pass. |
| tests/test_symlink_cycle_detection.py | Renames 'filtered' tests to 'display-only' and adds checks for target_outside_workspace=True and absence of target field; cycle-detection tests are unchanged. |
| tests/test_workspace_context_menu_and_rename.py | Adds a regression test verifying showPromptDialog always resets cancelBtn.style.display after a prior hideCancel call. |
| tests/test_workspace_symlink_tree_shape.py | Adds isExternalLink declaration, external-link icon, and drag-disable tests; TestSymlinkTooltip doesn't add a case for the external-link tooltip path. |
| tests/test_1707_workspace_filename_click.py | Threads isExternalLink into the JS-runner harness alongside the existing isLk/isDirLike locals; change is mechanical and correct. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[list_dir encounters symlink entry] --> B{reachable & no cycle?}
B -- No --> Z[return — drop entry]
B -- Yes --> C[resolve link_target]
C --> D{_is_blocked_system_path?}
D -- Yes --> Z
D -- No --> E{link_target inside workspace?}
E -- Yes --> F[emit full entry\nname, path, type, target,\nis_dir, size, mtime_ns]
E -- No --> G[emit display-only entry\nname, path, type,\nis_dir=False, mtime_ns\nNO target / size]
F --> H[Frontend: normal symlink row\nlink icon, rename, delete]
G --> I[Frontend: external-link icon\ndisplay-only, click opens info dialog]
G --> J[safe_resolve_ws still blocks\nnavigation through link]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[list_dir encounters symlink entry] --> B{reachable & no cycle?}
B -- No --> Z[return — drop entry]
B -- Yes --> C[resolve link_target]
C --> D{_is_blocked_system_path?}
D -- Yes --> Z
D -- No --> E{link_target inside workspace?}
E -- Yes --> F[emit full entry\nname, path, type, target,\nis_dir, size, mtime_ns]
E -- No --> G[emit display-only entry\nname, path, type,\nis_dir=False, mtime_ns\nNO target / size]
F --> H[Frontend: normal symlink row\nlink icon, rename, delete]
G --> I[Frontend: external-link icon\ndisplay-only, click opens info dialog]
G --> J[safe_resolve_ws still blocks\nnavigation through link]
Comments Outside Diff (1)
-
static/ui.js, line 14402-14405 (link)The tooltip on external-symlink name spans will read "Double-click to rename" even though double-clicking an external symlink opens the info dialog (not a rename prompt). The condition falls into
else if(!isDirLike)becauseisExternalLinkis not excluded —!isDirLikeistruefor external links (isDirLikeis forcedfalse), butitem.targetisundefined(intentionally omitted), so the symlink branch is skipped too. The result is a misleading affordance hint on every display-only row.
Reviews (1): Last reviewed commit: "docs(release): stamp v0.51.558 — escape-..." | Re-trigger Greptile
🎬 Cutter preview — PR #4610
|

Release v0.51.558 — Release TQ (workspace tree shows escape-target symlinks, display-only)
Ships #4581 (claw-io) — dedup-winner over #4573 (same feature, stronger tests). Security-surface feature on the symlink-containment boundary (#3398/#3450/#3630).
Added
Fixed
Gate
target, no target size, no target-derived dir state; external-link dialog uses a generic message (dropped{target}across all 13 locales).is_dir:falseas a render-hint (not target-derived, discloses nothing) for consistent frontend non-expandable rendering.Closes the #4573 duplicate. Thanks @claw-io (and @rodboev for the parallel #4573).