charmbracelet/crush · error

error checking file: %w

Error message

error checking file: %w

What it means

Before writing, the tool calls os.Stat on the target path to compare existing content and skip no-op writes. If Stat fails with an error other than NotExist (permissions, I/O error, path issues), the tool aborts and wraps the OS error. The write never proceeds because the tool cannot safely determine the file's state.

Source

Thrown at internal/agent/tools/write.go:86

			fileInfo, err := os.Stat(filePath)
			if err == nil {
				if fileInfo.IsDir() {
					return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
				}

				modTime := fileInfo.ModTime().Truncate(time.Second)
				lastRead := filetracker.LastReadTime(ctx, sessionID, filePath)
				if modTime.After(lastRead) {
					return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it.",
						filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
				}

				oldContent, readErr := os.ReadFile(filePath)
				if readErr == nil && string(oldContent) == params.Content {
					return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
				}
			} else if !os.IsNotExist(err) {
				return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
			}

			dir := filepath.Dir(filePath)
			if err = os.MkdirAll(dir, 0o755); err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
			}

			oldContent := ""
			if fileInfo != nil && !fileInfo.IsDir() {
				oldBytes, readErr := os.ReadFile(filePath)
				if readErr == nil {
					oldContent = string(oldBytes)
				}
			}

			diff, additions, removals := diff.GenerateDiff(
				oldContent,
				params.Content,

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Check permissions on every directory component of the target path (need +x on dirs).
  2. Verify the path is valid: no intermediate component is a regular file (ls each segment).
  3. If on a network/overlay mount, test Stat from the same user the agent runs as.
  4. As a fallback, choose a writable path (project working dir) instead of one outside it.

Example fix

// before
err := tool.Run(ctx, fantasy.ToolCall{Input: `{"file_path":"/etc/hosts.d/app.conf","content":"..."}`})

// after
// use a path inside the working directory the process can traverse
err := tool.Run(ctx, fantasy.ToolCall{Input: `{"file_path":"config/app.conf","content":"..."}`})
Defensive patterns

Strategy: try-catch

Validate before calling

if _, err := os.Stat(targetPath); err != nil && !os.IsNotExist(err) {
    return fmt.Errorf("cannot access %s: %w", targetPath, err)
}

Type guard

func statAccessible(path string) bool {
    _, err := os.Stat(path)
    return err == nil || os.IsNotExist(err)
}

Try / catch

var pathErr *fs.PathError
if errors.As(err, &pathErr) && errors.Is(err, fs.ErrPermission) {
    return fmt.Errorf("permission denied checking %s; fix directory execute bits", pathErr.Path)
}

Prevention

When it happens

Trigger: os.Stat(filePath) returns an error that is not os.IsNotExist — e.g. a parent directory in the path is not searchable (chmod 000), the path is on a failing mount, or an intermediate path component is a file, not a directory.

Common situations: Writing into a directory the agent user cannot traverse; ENOTDIR from a bad path segment; NFS/overlayfs glitches in containerized environments; SELinux/AppArmor denying access.

Related errors


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