pocketbase/pocketbase · warning
failed to resolve hooksDir symlink: %w
Error message
failed to resolve hooksDir symlink: %w
What it means
Returned by jsvm's watchHooks() (plugins/jsvm/jsvm.go:384) when os.Lstat shows that the configured hooks directory is a symlink but filepath.EvalSymlinks fails to resolve it. The watcher needs the real path so fsnotify events match; if the symlink is broken or loops, resolution fails. The caller logs this only as a yellow warning ('Unable to init hooks watcher'), so it degrades dev-mode hot reload rather than crashing.
Source
Thrown at plugins/jsvm/jsvm.go:384
// watchHooks initializes a hooks file watcher that will restart the
// application (*if possible) in case of a change in the hooks directory.
//
// This method does nothing if the hooks directory is missing.
func (p *plugin) watchHooks() error {
watchDir := p.config.HooksDir
hooksDirInfo, err := os.Lstat(p.config.HooksDir)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil // no hooks dir to watch
}
return err
}
if hooksDirInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
watchDir, err = filepath.EvalSymlinks(p.config.HooksDir)
if err != nil {
return fmt.Errorf("failed to resolve hooksDir symlink: %w", err)
}
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
var debounceTimer *time.Timer
stopDebounceTimer := func() {
if debounceTimer != nil {
debounceTimer.Stop()
debounceTimer = nil
}
}
p.app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {View on GitHub (pinned to 5d217ddb50)
Solutions
- Resolve or remove the dangling symlink: `readlink pb_hooks` to see the target, then recreate it (`ln -sfn /real/path pb_hooks`) or replace it with a real directory.
- Ensure the target path exists and the process user can traverse every component of it.
- Avoid symlink cycles (a → b → a); keep a single-level symlink to a real directory.
- If you do not need hot reload, leave HooksWatch disabled — watchHooks is not invoked and this error cannot occur.
Example fix
# before: broken symlink pb_hooks -> /mnt/shared/pb_hooks # volume deleted # after: point at an existing dir (or use a real dir) rm pb_hooks ln -sfn /srv/app/pb_hooks pb_hooks ls -lL pb_hooks # verify target resolves
Defensive patterns
Strategy: validation
Validate before calling
// Before enabling HooksWatch, confirm the hooks dir resolves to a real directory.
func resolvableHooksDir(dir string) (string, error) {
resolved, err := filepath.EvalSymlinks(dir)
if err != nil {
return "", fmt.Errorf("hooks dir does not resolve (broken symlink?): %w", err)
}
info, err := os.Stat(resolved)
if err != nil || !info.IsDir() {
return "", fmt.Errorf("resolved path %q is not a directory", resolved)
}
return resolved, nil
} Prevention
- Verify symlink targets with `ls -lL pb_hooks` after every environment change.
- Prefer bind-mounting real directories in Docker over symlinking into volumes.
- Avoid symlink chains and cycles; keep pb_hooks a single-hop link or a real dir.
- Remember this failure only degrades hot reload (warning log) — it will silently cost you watch mode in dev.
When it happens
Trigger: pb_hooks is a symlink whose target was deleted (broken symlink); a symlink chain with a cycle; missing search permission on a path component of the target; on some network filesystems where symlink evaluation returns IO errors.
Common situations: Docker deployments that symlink pb_hooks to a mounted volume and later remove the volume; monorepos where pb_hooks symlinks into a shared packages dir that a teammate has not cloned; macOS/Windows hosts with case-mismatched target names.
Related errors
- registerHooks: %w
- failed to execute %s:\n - %v
- registerMigrations: %w
- failed to run migration %s: %w
- failed to create migration dir: %w
AI-assisted analysis of pocketbase/pocketbase@5d217ddb50 (2026-08-15).
Data as JSON: /api/errors/bf64d8e6d61cbaea.
Report an issue: GitHub.