can1357/oh-my-pi · error
ssh://: ${remotePath} is a binary or non-UTF-8 file; ssh://
Error message
ssh://: ${remotePath} is a binary or non-UTF-8 file; ssh:// supports UTF-8 text only — use `bash` with a remote SSH command or an `sshfs` mount What it means
After a successful bounded read, the bytes are decoded with a fatal UTF-8 decoder that also rejects any NUL byte. If decoding fails, the file is binary or non-UTF-8 (e.g. Latin-1, images, executables), which ssh:// does not support — it only returns UTF-8 text.
Source
Thrown at packages/coding-agent/src/internal-urls/ssh-protocol.ts:306
return this.#resolveDirectory(target, remotePath, url, context?.signal, context?.skipDirectoryListing);
}
if (kind === "other") {
throw new Error(
`ssh://: ${remotePath} is not a regular file (FIFO, socket, or device); ssh:// reads UTF-8 text files only — use \`bash\` with a remote SSH command for special files`,
);
}
const fileResult = await readRemoteFile(target, remotePath, {
maxBytes: SSH_TEXT_MAX_BYTES,
signal: context?.signal,
});
if (fileResult.truncated) {
throw new Error(
`ssh://: ${remotePath} exceeds the 1 MiB limit; ssh:// supports text files up to 1 MiB — use an sshfs mount for larger files`,
);
}
const content = decodeUtf8Text(fileResult.bytes);
if (content === null) {
throw new Error(
`ssh://: ${remotePath} is a binary or non-UTF-8 file; ssh:// supports UTF-8 text only — use \`bash\` with a remote SSH command or an \`sshfs\` mount`,
);
}
// No `sourcePath`: keeps search on the virtual-resource path so the
// displayed/searched resource stays `ssh://…` instead of a temp path.
return {
url: url.href,
content,
contentType: contentTypeFor(remotePath),
size: fileResult.bytes.length,
};
}
/** Resolve a remote directory to a one-level listing (no `sourcePath`; `isDirectory` so search refuses it; immutable). */
async #resolveDirectory(
target: SSHConnectionTarget,
remotePath: string,
url: InternalUrl,View on GitHub (pinned to 9690622007)
Solutions
- Re-encode the file remotely to UTF-8 (e.g. `iconv -f latin1 -t utf-8`) and read the result
- Use the bash tool for binary inspection: `ssh prod 'xxd file | head'`, checksums, `file` output
- Mount via sshfs and use binary-capable local tooling
- Repair/re-download the file if the NUL indicates corruption
Example fix
// before
resolve('ssh://prod/data/legacy-iso8859.txt')
// after
bash("ssh prod 'iconv -f iso-8859-1 -t utf-8 /data/legacy.txt > /tmp/legacy-utf8.txt'")
resolve('ssh://prod/tmp/legacy-utf8.txt') Defensive patterns
Strategy: try-catch
Validate before calling
const head = await bash(`ssh ${host} head -c 4096 ${remotePath} | file -`);
if (!head.includes('text')) throw new Error(`${remotePath} looks binary; use bash/sshfs`); Try / catch
try {
const res = await handler.resolve(url, ctx);
} catch (e) {
if (e instanceof Error && e.message.includes('binary or non-UTF-8 file')) {
// inspect via bash (xxd/file) or iconv-convert to UTF-8 first
} else throw e;
} Prevention
- Verify remote files are UTF-8 text (`file -bi`) before reading via ssh://
- Convert legacy encodings with iconv on the remote before reading
- Never use ssh:// for executables, images, or archives — use bash or sshfs
When it happens
Trigger: Resolving an ssh:// URL to a file whose bytes contain invalid UTF-8 sequences or a NUL byte, so `decodeUtf8Text` returns null — e.g. `ssh://prod/usr/bin/tool` or a Latin-1-encoded text file.
Common situations: Pointing ssh:// at executables, images, archives; legacy text files in non-UTF-8 encodings (Windows-1252, ISO-8859-1, Shift-JIS); files with a stray NUL from corruption.
Related errors
- invalid utf-8 sequence
- invalid byte sequence: {:02x?}
- imageUrls exposure "ssh" requires imageUrls.publicBaseUrl
- imageUrls exposure "ssh" requires imageUrls.sshTarget
- ssh reverse forward to ${config.sshTarget} exited with code
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/f948564e0e900629.
Report an issue: GitHub.