gastownhall/beads · error

failed to stat %s: %w

Error message

failed to stat %s: %w

What it means

Wraps an os.Lstat failure in guardHookWritePath when the hook path exists but cannot be stat'd for a reason other than not-exist. guardHookWritePath pre-checks the target before bd writes a hook so it never clobbers symlinks or foreign tracked files.

Source

Thrown at cmd/bd/hooks.go:818

		return nil
	},
}

// guardHookWritePath refuses hook writes that would silently modify files
// bd does not own (bd-5vdt8):
//   - a symlinked hook path: os.WriteFile follows the link (O_TRUNC) and
//     rewrites the target inode — e.g. a repo-tracked script the hook
//     points at, dirtying every clone that shares it
//   - a hook file tracked by git: writing it dirties the working tree.
//     allowTracked exempts shared installs (.beads-hooks/ is deliberately
//     committed).
func guardHookWritePath(hookPath string, allowTracked bool) error {
	fi, err := os.Lstat(hookPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil // creating a new file — nothing to clobber
		}
		return fmt.Errorf("failed to stat %s: %w", hookPath, err)
	}
	if fi.Mode()&os.ModeSymlink != 0 {
		target := "unresolvable target"
		if resolved, rerr := filepath.EvalSymlinks(hookPath); rerr == nil {
			target = resolved
		} else if link, lerr := os.Readlink(hookPath); lerr == nil {
			target = link
		}
		return fmt.Errorf("%s is a symlink to %s; writing would rewrite the link target, not the hook\nRemove the symlink (or leave that hook to its owner) and re-run", hookPath, target)
	}
	if allowTracked {
		return nil
	}
	// A tracked file is refused only when bd does NOT own it: writing into a
	// foreign tracked file dirties every clone that shares it (the wy-81fnur
	// incident). A bd-owned hook the user chose to commit (e.g. a team-shared
	// .beads/hooks/) is bd's to maintain — same policy as shared installs.
	if isGitTrackedFile(hookPath) && !isBdOwnedHookFile(hookPath) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Fix permissions on the .git/hooks directory (chmod u+rx .git/hooks) and retry.
  2. Check the wrapped cause (%w) for the exact errno and address it (ownership, ACL, mount).
  3. Run outside restricted sandboxes or grant the process access to the repo's .git directory.

Example fix

// before
$ bd hooks install
// error: failed to stat .git/hooks/pre-commit: permission denied
// after
$ chmod u+rx .git/hooks
$ bd hooks install
Defensive patterns

Strategy: validation

Validate before calling

if fi, err := os.Lstat(hookPath); err != nil && !os.IsNotExist(err) {
	return fmt.Errorf("cannot access hook path %s: %w", hookPath, err)
}

Try / catch

if err := guardHookWritePath(hookPath, allowTracked); err != nil {
	if strings.Contains(err.Error(), "failed to stat") {
		// fix permissions or abort install with guidance
	}
}

Prevention

When it happens

Trigger: installHooksWithOptions or applyHookMigrationExecution calls guardHookWritePath(hookPath, allowTracked); os.Lstat fails with an error where os.IsNotExist(err) is false (e.g. permission denied on a parent directory).

Common situations: Parent .git/hooks directory lacks execute/read permission, sandboxed environment blocking stat, filesystem error, or a path component that is inaccessible.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/57d28b69c1cfdd4e. Report an issue: GitHub.