jackwener/OpenCLI · error · CommandExecutionError

Failed to attach file

Error message

Failed to attach file

What it means

sendWithFile() returned a failure result while attaching kwargs.file alongside the prompt; the library raises CommandExecutionError with the specific reason (or the generic 'Failed to attach file'). A thrown error is swallowed only if it matches 'Promise was collected', which actually indicates success.

Source

Thrown at clis/claude/ask.js:112

                throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
            }
            // Post-toggle settle dropped — the next CDP eval (setAdaptiveThinking) gives
            // React enough time to flush aria-checked updates between rountrips.
        }

        const thinkResult = await withRetry(() => setAdaptiveThinking(page, wantThink));
        if (!thinkResult?.ok && wantThink) {
            throw new CommandExecutionError('Could not enable Adaptive thinking');
        }
        // Post-toggle settle dropped — the next CDP eval (sendMessage / sendWithFile)
        // gives React enough time to flush aria-checked updates.

        if (kwargs.file) {
            const baseline = await withRetry(() => getBubbleCount(page));
            try {
                const fileResult = await sendWithFile(page, kwargs.file, prompt);
                if (fileResult && !fileResult.ok) {
                    throw new CommandExecutionError(fileResult.reason || 'Failed to attach file');
                }
            } catch (err) {
                // SPA navigates after send; "Promise was collected" means send succeeded
                if (!String(err?.message || err).includes('Promise was collected')) throw err;
            }
            // Pre-waitForResponse settle dropped — waitForResponse's first 3 s polling
            // tick covers the same window without an unconditional sleep.
            const result = await waitForResponse(page, baseline, prompt, timeoutMs);
            if (!result) {
                throw new EmptyResultError(
                    'claude ask',
                    `No Claude response appeared within ${timeoutSeconds}s. Re-run with a higher --timeout if the model is still generating.`,
                );
            }
            return [{ response: result }];
        }

        const baseline = await withRetry(() => getBubbleCount(page));

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Verify the --file path exists and the file type is supported (image, PDF, text).
  2. Retry the command — transient upload widget timing may resolve on a second run.
  3. Send the prompt without --file, or attach the file manually in the browser if automation keeps failing.

Example fix

// before
opencli claude ask "describe" --file ./reprot.pdf   # typo, file missing
// after
opencli claude ask "describe" --file ./report.pdf
Defensive patterns

Strategy: validation

Validate before calling

import { statSync } from 'fs';
const ALLOWED = /\.(png|jpe?g|gif|webp|pdf|txt|md)$/i;
function validateFile(p) {
  const s = statSync(p); // throws if missing
  if (s.size > 30 * 1024 * 1024) throw new Error('file too large for Claude upload');
  if (!ALLOWED.test(p)) throw new Error('unsupported file type');
}
validateFile(opts.file);

Try / catch

try {
  return await opencli.claude.ask(prompt, { file: p, timeout: 300 });
} catch (e) {
  if (/Failed to attach file|attach/i.test(e.message)) {
    return await opencli.claude.ask(prompt); // degrade to text-only
  }
  throw e;
}

Prevention

When it happens

Trigger: `claude ask ... --file photo.png` where the file input/upload fails: nonexistent file path, unsupported type, upload rejected by the page, or fileResult.ok false for any reason.

Common situations: Typo in the --file path; attaching a file type Claude rejects; file too large; SPA upload widget not ready when the automation interacts with it.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/a99e706de05e8501. Report an issue: GitHub.