gastownhall/beads · error
writing config: %w
Error message
writing config: %w
What it means
Config.Save() writes metadata.json via writeFileAtomic (temp file + rename) to guarantee readers never see a torn file. This error wraps any failure in that sequence: temp-file creation, write, chmod, close, or rename — typically environment/permission issues in the beads directory, not JSON problems.
Source
Thrown at internal/configfile/configfile.go:172
func (c *Config) Save(beadsDir string) error {
configPath := ConfigPath(beadsDir)
saved := *c
if filepath.IsAbs(saved.DoltDataDir) {
saved.DoltDataDir = ""
}
data, err := json.MarshalIndent(&saved, "", " ")
if err != nil {
return fmt.Errorf("marshaling config: %w", err)
}
// Write-temp-then-rename: a plain os.WriteFile truncates in place, so a
// concurrent Load can observe an empty or partial metadata.json and feed
// store selection a corrupt config. Rename within the same directory is
// atomic, so readers see either the old or the new file, never a torn one.
if err := writeFileAtomic(configPath, data, 0o600); err != nil {
return fmt.Errorf("writing config: %w", err)
}
return nil
}
// writeFileAtomic writes data to a temp file in path's directory and renames
// it over path, so concurrent readers never observe a truncated or partial
// file.
func writeFileAtomic(path string, data []byte, perm os.FileMode) error {
dir := filepath.Dir(path)
tmp, err := os.CreateTemp(dir, filepath.Base(path)+".tmp-*")
if err != nil {
return err
}
tmpName := tmp.Name()
defer os.Remove(tmpName) //nolint:errcheck // no-op after successful rename
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()View on GitHub (pinned to 71377f2769)
Solutions
- Verify the beads directory is writable by the current user (touch .beads/.probe) and fix with chown/chmod
- Free disk space / raise quota if the filesystem is full
- Remove immutability flags (chattr -i) or stale metadata.json.tmp-* files
- If on NFS or unusual mounts, move the workspace to a local filesystem or stop concurrent bd processes
Example fix
// before dr-xr-xr-x 2 user user .beads // after $ chmod u+w .beads && bd init
Defensive patterns
Strategy: try-catch
Validate before calling
info, err := os.Stat(beadsDir)
if err != nil || !info.IsDir() {
return fmt.Errorf("beads dir missing")
}
probe := filepath.Join(beadsDir, ".probe")
if f, err := os.Create(probe); err != nil {
return fmt.Errorf("beads dir not writable: %w", err)
} else {
f.Close(); os.Remove(probe)
} Try / catch
if err := cfg.Save(beadsDir); err != nil {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
// temp-create/rename failure: check space, perms, mount before retry
return fmt.Errorf("cannot write %s: %w", pathErr.Path, err)
}
return err
} Prevention
- Keep .beads on a local filesystem with atomic rename semantics
- Ensure free disk space before large bd operations
- Never mark metadata.json immutable or root-owned in shared workspaces
- Serialize concurrent bd processes per workspace to reduce temp-file churn
When it happens
Trigger: Calling Config.Save(beadsDir) when the beads directory is not writable, the disk is full, the directory was removed mid-save, or the target is on a filesystem where rename semantics fail (e.g. some network mounts, cross-device issues, or immutable files).
Common situations: Read-only .beads (mounted volume, checkout permissions); quota exceeded; metadata.json made immutable (chattr +i) or owned by root; NFS with stale handles; concurrent runs on a filesystem lacking atomic rename within the same directory.
Related errors
- failed to write user config.yaml: %w
- failed to create temp file: %w
- write metadata.json: %w
- failed to save config: %w
- failed to write .gitignore: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/4e3352c5e7553d97.
Report an issue: GitHub.