gastownhall/beads · error

removing already-retired sidecar %s: %w

Error message

removing already-retired sidecar %s: %w

What it means

This wraps os.Remove failure when deleting the source sidecar that has already been confirmed identical to an existing destination. The migration verified both files match, but unlinking the redundant source failed. The destination file remains intact; the source is left in place.

Source

Thrown at cmd/bd/migrate_hooks_apply.go:405

	if !sourceExists {
		return "", nil
	}

	destinationExists, err := pathExists(op.DestinationPath)
	if err != nil {
		return "", fmt.Errorf("checking sidecar destination %s: %w", op.DestinationPath, err)
	}

	if destinationExists {
		equal, err := filesEqual(op.SourcePath, op.DestinationPath)
		if err != nil {
			return "", fmt.Errorf("comparing sidecar %s to %s: %w", op.SourcePath, op.DestinationPath, err)
		}
		if !equal {
			return "", fmt.Errorf("artifact collision for %s: %s already exists with different content", op.SourcePath, op.DestinationPath)
		}
		if err := os.Remove(op.SourcePath); err != nil {
			return "", fmt.Errorf("removing already-retired sidecar %s: %w", op.SourcePath, err)
		}
		return op.SourcePath + " -> " + op.DestinationPath + " (destination already existed)", nil
	}

	if err := os.Rename(op.SourcePath, op.DestinationPath); err != nil {
		return "", fmt.Errorf("retiring sidecar %s -> %s: %w", op.SourcePath, op.DestinationPath, err)
	}

	return op.SourcePath + " -> " + op.DestinationPath, nil
}

func pathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if errors.Is(err, os.ErrNotExist) {
		return false, nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped error and permissions on the source's parent directory (write permission is what matters).
  2. chmod u+w the parent directory, or remove the immutable flag: chattr -i <file>.
  3. On Windows, close processes holding the file (AV, editors) and retry.
  4. Since destination content is already identical, manually deleting the source and re-running is safe.

Example fix

// before
dr-xr-xr-x /path/to/dir
// after
$ chmod u+w /path/to/dir
$ bd migrate
Defensive patterns

Strategy: validation

Validate before calling

fi, err := os.Stat(filepath.Dir(op.SourcePath))
if err != nil {
	return err
}
if fi.Mode()&0o200 == 0 {
	return fmt.Errorf("source dir %s not writable; os.Remove will fail", filepath.Dir(op.SourcePath))
}

Try / catch

if _, err := retireHookSidecar(op); err != nil {
	var pe *fs.PathError
	if errors.As(err, &pe) {
		return fmt.Errorf("cannot delete %s (check dir write perms / file locks): %w", pe.Path, pe.Err)
	}
	return err
}

Prevention

When it happens

Trigger: During `bd migrate` apply, os.Remove(op.SourcePath) fails — source directory lacks write permission (removing a file requires write on its directory), the file was locked by another process on Windows, or the directory is read-only.

Common situations: Source directory mounted read-only; parent directory owned by another user (no +w); Windows AV/indexer holding the file open; immutable attribute set on the file.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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