gastownhall/beads · error

fs: WriteBeadsGitignore: %w

Error message

fs: WriteBeadsGitignore: %w

What it means

This wraps a failure from os.WriteFile when creating .beads/.gitignore for the first time (the file did not previously exist). bd could not write the new file with mode 0600, usually because the .beads directory is not writable or a non-directory entry blocks the path.

Source

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

	if errors.Is(err, os.ErrNotExist) {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("fs: BeadsDirExists: stat %s: %w", r.beadsDir, err)
	}
	return info.IsDir(), nil
}

func (r *beadsDirFSRepositoryImpl) WriteBeadsGitignore(ctx context.Context) error {
	if r.templates.BeadsGitignore == "" {
		return fmt.Errorf("fs: WriteBeadsGitignore: template not configured")
	}
	path := filepath.Join(r.beadsDir, ".gitignore")
	// #nosec G304 -- path joined under bound beadsDir
	existing, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		if werr := os.WriteFile(path, []byte(r.templates.BeadsGitignore), 0600); werr != nil {
			return fmt.Errorf("fs: WriteBeadsGitignore: %w", werr)
		}
		return nil
	}
	if err != nil {
		return fmt.Errorf("fs: WriteBeadsGitignore: read: %w", err)
	}
	// Existing file: append-only. A wholesale rewrite to the template
	// destroys local rules (e.g. export negations) the user added — the
	// gitignore is shared state, not bd-owned (bd-kaaz3).
	missing := missingTemplatePatternLines(string(existing), r.templates.BeadsGitignore)
	if len(missing) == 0 {
		return nil
	}
	content := string(existing)
	if len(content) > 0 && !strings.HasSuffix(content, "\n") {
		content += "\n"
	}
	content += "\n# Added by bd (missing required patterns)\n" + strings.Join(missing, "\n") + "\n"

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure the .beads directory is writable by the current user (chown/chmod it)
  2. Remove any directory or stale entry occupying the .gitignore path inside .beads
  3. Free disk space / raise quota if the wrapped cause is ENOSPC
  4. If the volume is read-only, remount read-write or run bd from a writable checkout

Example fix

// before
$ sudo chown root .beads
$ bd init  // fs: WriteBeadsGitignore: open .beads/.gitignore: permission denied
// after
$ sudo chown $(whoami) .beads
$ bd init  # .gitignore created
Defensive patterns

Strategy: validation

Validate before calling

if info, err := os.Stat(beadsDir); err == nil {
	if err := unix.Access(beadsDir, unix.W_OK); err != nil {
		return fmt.Errorf("%s not writable: %v", beadsDir, err)
	}
	if _, err := os.Lstat(filepath.Join(beadsDir, ".gitignore")); err == nil {
		if fi, err := os.Stat(filepath.Join(beadsDir, ".gitignore")); err == nil && fi.IsDir() {
			return errors.New(".beads/.gitignore is a directory")
		}
	}
}
repo.WriteBeadsGitignore(ctx)

Type guard

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

Try / catch

if err := repo.WriteBeadsGitignore(ctx); err != nil {
	var pe *fs.PathError
	if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EACCES) {
		return fmt.Errorf("make %s writable: chown -R $(whoami) %s", filepath.Dir(pe.Path), filepath.Dir(pe.Path))
	}
	return err
}

Prevention

When it happens

Trigger: Calling WriteBeadsGitignore when .beads/.gitignore does not exist and os.WriteFile(path, ..., 0600) fails: permission denied on the .beads dir, path is a directory, disk full, or read-only filesystem.

Common situations: .beads mounted read-only (container volume); directory owned by another user; a directory named .gitignore sits in the way; quota exceeded on the volume.

Related errors


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