can1357/oh-my-pi · error

ssh:// does not support URL fragments; percent-encode a lite

Error message

ssh:// does not support URL fragments; percent-encode a literal '#' as %23 in the path: ${url.href}

What it means

In a URL, `#` starts a fragment and the parser strips it and everything after it from the path. ssh:// remote paths have no fragment semantics, so the handler rejects any URL containing a fragment instead of silently operating on the truncated path; a literal `#` in a remote filename must be percent-encoded as `%23`.

Source

Thrown at packages/coding-agent/src/internal-urls/ssh-protocol.ts:84

/**
 * Remote absolute path from the URL. Uses `rawPathname` (pre-normalization) so
 * `..`/`//` and percent-escapes survive verbatim to the remote shell; the
 * authority (host/user/port) stays on the WHATWG fields, which preserve case for
 * the non-special `ssh` scheme.
 */
function remotePathFromUrl(url: InternalUrl): string {
	// `?`/`#` are URL delimiters, so parseInternalUrl strips them from the path
	// (`ssh://h/tmp/a?draft` → `/tmp/a`). Reject the unsupported suffix instead of
	// silently operating on the truncated path; a literal `?`/`#` in a filename
	// must be percent-encoded (`%3F`/`%23`).
	if (url.search) {
		throw new Error(
			`ssh:// does not support URL query strings; percent-encode a literal '?' as %3F in the path: ${url.href}`,
		);
	}
	if (url.hash) {
		throw new Error(
			`ssh:// does not support URL fragments; percent-encode a literal '#' as %23 in the path: ${url.href}`,
		);
	}
	const raw = url.rawPathname ?? url.pathname;
	let decoded: string;
	try {
		decoded = decodeURIComponent(raw);
	} catch {
		throw new Error(`Invalid URL encoding in ssh:// path: ${url.href}`);
	}
	if (!decoded) {
		throw new Error(
			"ssh:// requires an absolute path, e.g. ssh://host/etc/hosts or ssh://host/ for the root directory",
		);
	}
	return decoded;
}

View on GitHub (pinned to 9690622007)

Solutions

  1. Percent-encode the literal `#` as `%23`: `ssh://host/etc/config%23backup`.
  2. Strip any fragment you appended for HTTP-style anchors — it has no meaning for ssh paths.
  3. Rename the remote file to avoid `#` if encoding is inconvenient in your tooling.

Example fix

// before
await readResource("ssh://host/docs/notes#2.txt");
// after
await readResource("ssh://host/docs/notes%232.txt");
Defensive patterns

Strategy: validation

Validate before calling

function assertNoSshFragment(url: string): void {
  const u = new URL(url);
  if (u.protocol === "ssh:" && u.hash) {
    throw new Error(`ssh:// URL must not contain a fragment: ${url}`);
  }
}

Try / catch

try {
  return await sshHandler.resolve(url);
} catch (err) {
  if (err instanceof Error && err.message.includes("does not support URL fragments")) {
    return sshHandler.resolve(parseInternalUrl(url.replaceAll("#", "%23")));
  }
  throw err;
}

Prevention

When it happens

Trigger: `SshProtocolHandler.resolve()` or `.write()` (via `remotePathFromUrl`) with a `ssh://` URL whose `url.hash` is non-empty, e.g. `ssh://host/etc/issue#1` or a filename like `notes#2.txt` passed unencoded.

Common situations: POSIX filenames containing `#` (common for scratch/versioned files like `config#backup`) used in ssh:// URLs; markdown-style anchor fragments appended by habit; copy-pasting paths containing `#` without encoding.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/f8ec1ba93af3a19a. Report an issue: GitHub.