gastownhall/beads · error
refusing to chmod %s: path is a symbolic link
Error message
refusing to chmod %s: path is a symbolic link
What it means
fixBeadsDirPermissions refuses to chmod the .beads directory when the given path is a symbolic link. The library protects against following symlinks during a permission repair, since a swapped symlink could redirect a privileged chmod onto an unintended target. The operation is intentionally aborted rather than resolved.
Source
Thrown at internal/config/permissions.go:57
return fixBeadsDirPermissions(path, openBeadsDirHandle)
}
type beadsDirHandle interface {
Stat() (os.FileInfo, error)
Chmod(os.FileMode) error
Close() error
}
func fixBeadsDirPermissions(path string, openDir func(string) (beadsDirHandle, error)) (bool, error) {
info, err := os.Lstat(path)
if err != nil {
if os.IsNotExist(err) {
return false, nil // directory doesn't exist yet
}
return false, fmt.Errorf("failed to inspect %s: %w", path, err)
}
if info.Mode()&os.ModeSymlink != 0 {
return false, fmt.Errorf("refusing to chmod %s: path is a symbolic link", path)
}
if !info.IsDir() {
return false, fmt.Errorf("refusing to chmod %s: path is not a directory", path)
}
perm := info.Mode().Perm()
if perm&0077 == 0 {
return false, nil // no group or world-accessible bits
}
dir, err := openDir(path)
if err != nil {
return false, fmt.Errorf("failed to open %s securely: %w", path, err)
}
defer func() { _ = dir.Close() }()
openedInfo, err := dir.Stat()
if err != nil {
return false, fmt.Errorf("failed to inspect opened directory %s: %w", path, err)View on GitHub (pinned to 71377f2769)
Solutions
- Replace the symlink with a real directory: rm the link, mkdir .beads, and migrate contents back manually.
- If intentionally symlinked, chmod the real target directory yourself (chmod 700 <target>) instead of calling FixBeadsDirPermissions on the link.
- Verify with ls -la that .beads is a plain directory before re-running the fix.
Example fix
// before: .beads -> /mnt/shared/beads (symlink) // after $ rm .beads $ mkdir .beads && chmod 700 .beads $ cp -a /mnt/shared/beads/. .beads/
Defensive patterns
Strategy: validation
Validate before calling
info, err := os.Lstat(beadsDir)
if err != nil {
return err // not exists: nothing to fix
}
if info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("%s is a symlink; fix the target manually", beadsDir)
} Type guard
func isRealDir(path string) bool {
info, err := os.Lstat(path)
return err == nil && info.IsDir() && info.Mode()&os.ModeSymlink == 0
} Prevention
- Never symlink .beads; keep it a real directory in each repo.
- Run `ls -la` on .beads if provisioning scripts touched it.
- Treat symlink-swap detection as a security signal, not just an error.
When it happens
Trigger: Calling FixBeadsDirPermissions(path) when os.Lstat(path) reports os.ModeSymlink — i.e. the .beads path itself was replaced by a symlink (commonly a symlink swap attack or the user symlinked .beads to another location).
Common situations: A user replaced .beads with a symlink to a shared or dotfiles-managed location; a malicious actor swapped the directory for a symlink mid-repair; a provisioning script created .beads as a link instead of a real directory.
Related errors
- failed to open %s securely: %w
- dolt path is not executable
- failed to create backup directory: %w
- failed to create temp file: %w
- create beads directory: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/401db63f311d584e.
Report an issue: GitHub.