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
- Fix the wrapped cause: chmod u+r <workDir>/.gitignore or correct its ownership
- Replace a broken symlink with a normal file; bd will recreate and merge the required patterns
- Retry after resolving transient filesystem/lock conditions
- 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
- Keep the project .gitignore owned and readable by the user running bd
- Avoid symlink chains for the project .gitignore
- Verify permissions after copying repos between machines or restore operations
- Close/avoid concurrent tools that rewrite .gitignore with restrictive modes
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
- fs: WriteBeadsGitignore: read: %w
- ensure .beads/.gitignore: %w
- read .beads/.gitignore: %w
- failed to read .gitignore: %w
- failed to write .gitignore: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/66cb2fe291be529b.
Report an issue: GitHub.