schollz/croc · error · Error

Remote path is not allowed: ${value}

Error message

Remote path is not allowed: ${value}

What it means

normalizeFolder() rejects any normalized folder whose joined path contains the substring '.ssh'. This blocks a malicious transfer from overwriting the receiver's SSH keys/authorized_keys, a classic remote-code-execution vector when downloads land in or near the user's home directory.

Source

Thrown at web/src/protocol/metadata.ts:31

  for (const segment of replaced.split("/")) {
    if (segment === "" || segment === ".") continue;
    if (segment === "..") throw new Error(`Remote path escapes the destination: ${value}`);
    if ([...segment].some((character) => !/\P{C}/u.test(character))) {
      throw new Error(`Remote path contains a non-printable character: ${value}`);
    }
    segments.push(segment);
  }
  return segments;
}

export function normalizeFolder(value = ".") {
  if (/^(?:[a-zA-Z]:|\/)/.test(value)) {
    throw new Error(`Remote path must be relative: ${value}`);
  }
  const segments = cleanSegments(value);
  const normalized = segments.join("/") || ".";
  if (normalized.includes(".ssh")) {
    throw new Error(`Remote path is not allowed: ${value}`);
  }
  return normalized;
}

export function normalizeFilePath(folderValue: string, nameValue: string) {
  const folder = normalizeFolder(folderValue);
  const nameSegments = cleanSegments(nameValue);
  if (nameSegments.length !== 1 || nameSegments[0] !== nameValue.replaceAll("\\", "/")) {
    throw new Error(`Remote filename must be a basename: ${nameValue}`);
  }
  const name = nameSegments[0];
  if (!name) throw new Error("Remote filename is empty");
  const path = folder === "." ? name : `${folder}/${name}`;
  return { folder, name, path };
}

export function normalizeOutgoingFileName(value: string) {
  // Go's unicode.IsPrint accepts ASCII space but rejects the other Unicode

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Exclude .ssh from any folder-sync source on the sender before offering.
  2. As a receiver, refuse the transfer when this fires — the guard is intentional and must not be bypassed.
  3. If you truly need to transfer SSH configs, use a renamed folder and restore manually after the transfer.
Defensive patterns

Strategy: validation

Validate before calling

function mentionsSsh(normalizedFolder: string): boolean {
  return normalizedFolder.includes(".ssh");
}
// after your own normalization, before accepting:
if (mentionsSsh(folder)) rejectOffer(".ssh destinations are blocked");

Try / catch

try {
  const folder = normalizeFolder(value);
} catch (error) {
  if (error instanceof Error && error.message.startsWith("Remote path is not allowed")) {
    rejectOffer("path touches .ssh"); // intentional guard — never bypass
    return;
  }
  throw error;
}

Prevention

When it happens

Trigger: An offer folder that normalizes to something containing '.ssh' anywhere — e.g. '.ssh', 'backup/.ssh', or 'x.ssh.y'. The check is substring-based on the joined normalized path, so it is intentionally broad.

Common situations: Hostile sender attempting SSH key overwrite; a user legitimately syncing a dotfiles repo that includes .ssh (must be excluded); security scanners asserting the guard fires.

Related errors


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/a9e7d5aabb11a7bd. Report an issue: GitHub.