charmbracelet/crush · error

failed to create output file: %w

Error message

failed to create output file: %w

What it means

os.Create failed when opening/creating the destination file for the download. Parent directories were already created successfully, so this is specific to the final path component: the wrapped OS error gives the reason (permission denied, is-a-directory, too many open files, disk full at open time).

Source

Thrown at internal/agent/tools/download.go:145

			resp, err := client.Do(req)
			if err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("failed to download from URL: %w", err)
			}
			defer resp.Body.Close()

			if resp.StatusCode != http.StatusOK {
				return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
			}

			// Create parent directories if they don't exist
			if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
			}

			// Create the output file
			outFile, err := os.Create(filePath)
			if err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("failed to create output file: %w", err)
			}
			defer outFile.Close()

			// Copy data without an explicit size limit.
			// The overall download is still constrained by the HTTP client's timeout
			// and any upstream server limits.
			bytesWritten, err := io.Copy(outFile, resp.Body)
			if err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
			}

			contentType := resp.Header.Get("Content-Type")
			responseMsg := fmt.Sprintf("Successfully downloaded %d bytes to %s", bytesWritten, relPath)
			if contentType != "" {
				responseMsg += fmt.Sprintf(" (Content-Type: %s)", contentType)
			}

			return fantasy.NewTextResponse(responseMsg), nil

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Ensure file_path is not an existing directory and is a valid filename
  2. Check write permission on the target directory
  3. Close leaked file handles if fd limits are involved (ulimit -n)
  4. Check the wrapped error (EISDIR, EACCES, ENAMETOOLONG) and correct the path accordingly

Example fix

// before
file_path: "dist"            // dist is a directory
// after
file_path: "dist/app.tar.gz"
Defensive patterns

Strategy: validation

Validate before calling

if fi, err := os.Stat(target); err == nil && fi.IsDir() {
    return fmt.Errorf("%s is a directory; pick a file name", target)
}
if f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, 0o644); err != nil {
    return fmt.Errorf("cannot create %s: %v", target, err)
} else { f.Close() }

Try / catch

outFile, err := os.Create(filePath)
if err != nil {
    switch {
    case errors.Is(err, fs.ErrPermission):
        return fmt.Errorf("no permission to create %s", filePath)
    case errors.Is(err, syscall.EISDIR):
        return fmt.Errorf("%s is a directory", filePath)
    default:
        return err
    }
}

Prevention

When it happens

Trigger: filePath itself is an existing directory, the process lacks write permission on the containing directory, the path exceeds NAME_MAX, or the process hit its file-descriptor limit.

Common situations: file_path names an existing directory; downloading into a directory without write access; downloading many files concurrently exhausting ulimit -n; filename with illegal characters (e.g. '/' embedded, NUL).

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/e9b4fb618df098b7. Report an issue: GitHub.