gastownhall/beads · error

fs: WriteProjectGitignore: read: %w

Error message

fs: WriteProjectGitignore: read: %w

What it means

Before merging, WriteProjectGitignore reads the existing project-root .gitignore so it can append only missing patterns. This error wraps an os.ReadFile failure other than ErrNotExist on <workDir>/.gitignore — bd could not read a file that appears to exist.

Source

Thrown at internal/storage/domain/fs/beads.go:154

	return missing
}

func (r *beadsDirFSRepositoryImpl) BeadsGitignoreExists(ctx context.Context) (bool, error) {
	return fileExists(filepath.Join(r.beadsDir, ".gitignore"), "fs: BeadsGitignoreExists")
}

func (r *beadsDirFSRepositoryImpl) WriteProjectGitignore(ctx context.Context) error {
	if r.workDir == "" {
		return fmt.Errorf("fs: WriteProjectGitignore: workDir not set")
	}
	if len(r.templates.ProjectGitignorePatterns) == 0 {
		return fmt.Errorf("fs: WriteProjectGitignore: patterns not configured")
	}
	path := filepath.Join(r.workDir, ".gitignore")
	// #nosec G304 -- path joined under bound workDir
	existing, err := os.ReadFile(path)
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("fs: WriteProjectGitignore: read: %w", err)
	}

	var toAdd []string
	for _, pattern := range r.templates.ProjectGitignorePatterns {
		if !containsLine(existing, pattern) {
			toAdd = append(toAdd, pattern)
		}
	}
	if len(toAdd) == 0 {
		return nil
	}

	var buf bytes.Buffer
	buf.Write(existing)
	if len(existing) > 0 && !bytes.HasSuffix(existing, []byte("\n")) {
		buf.WriteByte('\n')
	}
	if header := r.templates.ProjectGitignoreHeader; header != "" && !containsLine(existing, header) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Fix the wrapped cause: chmod u+r <workDir>/.gitignore or correct its ownership
  2. Replace a broken symlink with a normal file; bd will recreate and merge the required patterns
  3. Retry after resolving transient filesystem/lock conditions
  4. As a last resort, move the unreadable file aside, run the write, then re-apply your custom rules

Example fix

// before
$ ls -l .gitignore  # -rw------- root
$ bd init  // fs: WriteProjectGitignore: read: permission denied
// after
$ sudo chown $(whoami) .gitignore && chmod u+rw .gitignore
$ bd init  # missing bd patterns appended
Defensive patterns

Strategy: validation

Validate before calling

p := filepath.Join(workDir, ".gitignore")
if fi, err := os.Stat(p); err == nil && !fi.Mode().Perm()&0o400 != 0 {
	return fmt.Errorf("%s unreadable; chmod u+r %s", p, p)
}
repo.WriteProjectGitignore(ctx)

Type guard

func isReadErr(err error) bool {
	var pe *fs.PathError
	return errors.As(err, &pe) && pe.Op == "open"
}

Try / catch

if err := repo.WriteProjectGitignore(ctx); err != nil {
	if strings.Contains(err.Error(), ": read: ") {
		return fmt.Errorf("%s/.gitignore unreadable; fix ownership/permissions or replace broken symlink", workDir)
	}
	return err
}

Prevention

When it happens

Trigger: Calling WriteProjectGitignore when workDir/.gitignore exists but os.ReadFile fails: EACCES, EISDIR, ELOOP from a symlink cycle, or transient I/O error.

Common situations: Project .gitignore owned by root or another user with restrictive mode; a symlinked gitignore pointing outside the repo with bad permissions; concurrent tooling locking or removing the file on a network FS.

Related errors


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