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

  1. 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.
  2. Ensure the target path exists and the process user can traverse every component of it.
  3. Avoid symlink cycles (a → b → a); keep a single-level symlink to a real directory.
  4. 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

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


AI-assisted analysis of pocketbase/pocketbase@5d217ddb50 (2026-08-15). Data as JSON: /api/errors/bf64d8e6d61cbaea. Report an issue: GitHub.