charmbracelet/crush · error

error checking file

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.

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.

Appendix: 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)