can1357/oh-my-pi · error · Error

images.urls.command must reference {file}

Error message

images.urls.command must reference {file}

What it means

createCommandUploader requires every command template to include the {file} placeholder, which is substituted with the temporary file path holding the upload bytes. Without {file} the command would never receive the blob to upload, so it throws at creation time. This enforces that the command actually transfers the payload.

Source

Thrown at packages/coding-agent/src/blob-broker/uploaders.ts:84

/** Last URL printed on stdout wins; uploader tools often log progress first. */
export function extractUploadUrl(stdout: string): string | null {
	let last: string | null = null;
	for (const match of stdout.matchAll(URL_PATTERN)) {
		last = match[0].replace(/[)\],.'"]+$/, "");
	}
	return last;
}

/**
 * Build an uploader from an argv template. Placeholders, substituted after
 * splitting (paths with spaces stay one argument): `{file}` temp file path,
 * `{mime}` MIME type, `{ext}` bare extension.
 */
export function createCommandUploader(template: string): BlobUploader {
	const argvTemplate = splitCommandTemplate(template);
	if (argvTemplate.length === 0) throw new Error("images.urls.command is empty");
	if (!argvTemplate.some(arg => arg.includes("{file}"))) {
		throw new Error("images.urls.command must reference {file}");
	}
	return {
		destination: "command",
		async upload(request: BlobUploadRequest): Promise<BlobPublication> {
			const { bytes, mimeType, extension } = request;
			const file = path.join(os.tmpdir(), `omp-blob-upload-${crypto.randomUUID()}.${extension}`);
			await Bun.write(file, bytes);
			try {
				const argv = argvTemplate.map(arg =>
					arg.replaceAll("{file}", file).replaceAll("{mime}", mimeType).replaceAll("{ext}", extension),
				);
				const cwd = getProjectDir();
				if (!directoryIsEnterableSync(cwd)) {
					throw new Error(`Project directory is not accessible: ${cwd}`);
				}
				const proc = Bun.spawn(argv, { stdin: "ignore", stdout: "pipe", stderr: "pipe", cwd });
				const timeout = setTimeout(() => proc.kill(), UPLOAD_TIMEOUT_MS);
				const [stdout, stderr, exitCode] = await Promise.all([

View on GitHub (pinned to 9690622007)

Solutions

  1. Add {file} to the command where the upload file path belongs, e.g. "curl -F 'file=@{file}' https://0x0.st"
  2. If the tool reads from stdin, wrap it so it accepts a file argument (e.g. "sh -c 'tool < {file}'" won't work via placeholder substitution — use a tool that takes a path)
  3. Confirm the placeholder braces are literal {file} (no extra spaces like { file })
  4. Check splitting behavior: the placeholder can be inside a quoted argument

Example fix

// before
"urls": { "command": "transferwee https://transfer.sh" }
// after
"urls": { "command": "transferwee upload {file}" }
Defensive patterns

Strategy: validation

Validate before calling

const cmd = config.images?.urls?.command;
if (typeof cmd === "string" && !cmd.includes("{file}")) {
	throw new Error("images.urls.command must reference {file}");
}

Try / catch

try {
	const uploader = createConfiguredUploader("command", config);
} catch (err) {
	if (err instanceof Error && err.message.includes("must reference {file}")) {
		// rewrite the command to include {file} where the upload path belongs
	}
	throw err;
}

Prevention

When it happens

Trigger: Configuring images.urls.command with a command lacking {file}, e.g. "images.urls.command": "curl -d @- https://host" or "upload.sh" — any template where no argument contains the literal {file}.

Common situations: Writing a command designed for stdin input instead of a file argument; forgetting the placeholder when adapting an existing upload script; copy-pasting a command from docs that reads the path from an environment variable instead.

Related errors


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