[BUG] OSC 8 file-path hyperlinks use a scheme-less URI → macOS "The application can't be opened. -50"

Status Closed — not planned
Reported on v2.1.212
Maintainer reply None cached
Activity 2 comments · opened Jul 17, 2026 · closed Jul 17, 2026

Summary

Claude Code emits OSC 8 hyperlinks for file paths whose URI has no scheme (a bare /Users/... path rather than file:///Users/...). On macOS, handing LaunchServices a scheme-less URL fails with paramErr (-50), so CMD+Click in iTerm2 produces:

The application can't be opened. -50

This is distinct from #78109. That issue concerns pathToFileURL(path + ':' + line) percent-escaping the colon, which produces a well-formed file:// URI pointing at a nonexistent file (LaunchServices NSCocoaErrorDomain 260, "no such file"). This one has no scheme at all and fails earlier with a different error.

Environment

  • Claude Code 2.1.212 (native installer build)
  • macOS 26 (Darwin 25.5.0)
  • iTerm2 3.6.11
  • Reproduces with and without tmux, and with and without a :line suffix

Evidence

1. The rendered link has no scheme. Holding CMD and hovering a file path in Claude Code's output, iTerm2 shows the link target as /Users/me/.claude/skills/monitor-setup/SKILL.md — no file:// prefix. A hand-made OSC 8 link to the same file shows file:///Users/... on hover and opens correctly:

# works — opens the file
printf 'A \033]8;;file:///Users/me/.claude/skills/monitor-setup/SKILL.md\033\\CLICK\033]8;;\033\\\n'

2. -50 is an app-launch failure, not a file failure. Probing NSWorkspace.open(_:configuration:completionHandler:) directly on this machine:

| input | result |
|---|---|
| file:///…/SKILL.md | opens correctly |
| file:///…/SKILL.md:42 | NSCocoaErrorDomain 260 (no such file) |
| /…/SKILL.md (scheme-less) | NSCocoaErrorDomain 256 |

None of these yield -50 — because NSWorkspace.open parses the URL itself. The -50 arises in the LaunchServices path that iTerm2 uses, consistent with the Ghostty capture below.

3. Two different OSC 8 emitters exist in the bundle. Minified, from the 2.1.212 binary (names are from the compiled bundle and won't map to source):

// (a) always produces a scheme — this is #78109's site:
function e7(e){ if(!y0()) return e;
  return `\x1B]8;;${UBd.pathToFileURL(e).href}\x07${e}\x1B]8;;\x07` }

// (b) interpolates the raw string as the URI — no scheme conversion:
var O2d="\x1B]8;;", P2d="\x07";
let u = (((r?.themeName)?wQn(r.themeName):!1)?gt.blue:gt.blueBright)(t??e);
return `${O2d}${e}${P2d}${u}${O2d}${P2d}`;

Site (b) uses e verbatim as the hyperlink target. When e is a filesystem path rather than a URL, the emitted OSC 8 URI is scheme-less. This matches the observed hover text exactly.

Prior art: Ghostty hit this and fixed it terminal-side

ghostty-org/ghostty#8599 / #9037 — "Claude Code Link to local files fails to open file: The application can't be opened -50" — fixed in Ghostty 1.2.1 via PR #8764. Their captured log confirms the mechanism:

LAUNCH: Launch failed in CSUI with error Error Domain=NSOSStatusErrorDomain
Code=-50 "paramErr: error in user parameter list"

…via _LSOpenStuffCallLocal in CoreServicesUIAgent. Their fix:

- guard let url = URL(string: action.url) else { ... }
+ // The URL initializer will gladly take invalid URLs (e.g. plain file paths) and turn
+ // them into schema-less URLs, but these won't open properly in text editors.
+ if let candidate = URL(string: action.url), candidate.scheme != nil { url = candidate }
+ else { url = URL(filePath: action.url) }

Ghostty defends against it; iTerm2 does not. The emitted URI is malformed either way.

Suggested fix

Ensure every OSC 8 URI carries a scheme. At minimum, site (b) should go through pathToFileURL() (or reject non-URL input) rather than interpolating a raw path.

Worth pairing with #78109's fix — emitting #line as a fragment rather than :line would additionally let iTerm2 route these through Semantic History, which resolves line numbers correctly (per iTerm2's docs, file: URLs with a # fragment are handled by Semantic History; file:///tmp/file.txt#123). A plain file:// URI cannot express a line number at all.

Workaround

FORCE_HYPERLINK=0 disables both emitters (y0() gate). It's the only lever — there's no settings.json key; FE() in the gate resolves to attacherCaps, not user config.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗