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

  1. Verify the beads directory is writable by the current user (touch .beads/.probe) and fix with chown/chmod
  2. Free disk space / raise quota if the filesystem is full
  3. Remove immutability flags (chattr -i) or stale metadata.json.tmp-* files
  4. 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

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


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