gastownhall/beads · error

failed to read .gitignore: %w

Error message

failed to read .gitignore: %w

What it means

removeBeadsProjectGitignoreSection reads the repo-root .gitignore so it can strip the beads-managed section during stealth-mode cleanup. If os.ReadFile fails with anything other than fs.ErrNotExist (which is treated as "nothing to do" and returns nil), the underlying error is wrapped with %w and returned to applyFixList. This means the .gitignore exists in name but could not actually be read.

Source

Thrown at cmd/bd/init_stealth.go:178

	}
	return nil
}

// removeBeadsProjectGitignoreSection strips the bd-managed section from the tracked project-root
// .gitignore at repoPath, reversing doctor.EnsureProjectGitignore. It removes only the header beads
// writes (doctor.ProjectGitignoreHeader) plus the Dolt pattern lines beads added directly beneath
// it, so unrelated user patterns are preserved. If beads was the .gitignore's only content the file
// is removed entirely, restoring true stealth. Returns true when it changed (or removed) the file;
// a repo with no beads section (or no .gitignore) is left untouched.
func removeBeadsProjectGitignoreSection(repoPath string) (bool, error) {
	gitignorePath := filepath.Join(repoPath, ".gitignore")
	// #nosec G304 - path is the repo-root .gitignore
	content, err := os.ReadFile(gitignorePath)
	if err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, fmt.Errorf("failed to read .gitignore: %w", err)
	}

	managed := make(map[string]bool, len(doctor.ProjectGitignorePatterns))
	for _, p := range doctor.ProjectGitignorePatterns {
		managed[p] = true
	}

	lines := strings.Split(string(content), "\n")
	out := make([]string, 0, len(lines))
	changed := false
	for i := 0; i < len(lines); i++ {
		if strings.TrimSpace(lines[i]) == doctor.ProjectGitignoreHeader {
			changed = true
			// Drop the blank separator beads writes before the header, if we just emitted one.
			if n := len(out); n > 0 && strings.TrimSpace(out[n-1]) == "" {
				out = out[:n-1]
			}
			// Skip the header and the bd-managed pattern lines directly beneath it.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check and fix file permissions: ls -l .gitignore, then chmod u+r .gitignore (or chown to the user running bd).
  2. Verify .gitignore is a regular file, not a directory or broken symlink: file .gitignore; remove/replace it if malformed.
  3. Re-run the command from the repository root so gitignorePath resolves to the correct .gitignore.
  4. If on a flaky network mount, retry after the mount is healthy, or copy the repo locally.
  5. As a last resort, recreate a minimal .gitignore with correct ownership and re-run bd stealth cleanup.

Example fix

// before (permissions blocking the read)
$ ls -l .gitignore
-rw------- 1 root root 412 .gitignore

// after
$ sudo chown $(whoami) .gitignore && chmod 644 .gitignore
$ bd doctor --fix   # removeBeadsProjectGitignoreSection now reads the file
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Stat(gitignorePath)
if err == nil && !info.IsDir() {
	if f, err := os.OpenFile(gitignorePath, os.O_RDONLY, 0); err == nil {
		f.Close() // readable; removeBeadsProjectGitignoreSection will not fail on read
	} else {
		fmt.Printf(".gitignore not readable: %v\n", err)
	}
}

Try / catch

changed, err := removeBeadsProjectGitignoreSection(gitignorePath)
if err != nil {
	var perr *fs.PathError
	if errors.As(err, &perr) && errors.Is(perr.Err, syscall.EACCES) {
		return fmt.Errorf("cannot read %s: check file permissions/ownership: %w", gitignorePath, err)
	}
	return err
}

Prevention

When it happens

Trigger: os.ReadFile(gitignorePath) returns an error that is not os.IsNotExist: permission denied on the file, the path is a directory instead of a file, an I/O error occurs, or the path is too long / on a failing mount.

Common situations: Running `bd doctor` / stealth fix under a different user than the one who owns .gitignore (e.g. sudo'd agent runs); .gitignore has restrictive modes (0600 owned by root); CI containers where the repo was mounted with odd permissions; a stale symlink pointing at a nonexistent or unreadable target (symlink errors are not ErrNotExist in all cases); NFS/Network mounts dropping reads.

Related errors


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