crowdsecurity/crowdsec · error

unable to symlink postoverflow

Error message

unable to symlink postoverflow '%s' to '%s': %w

What it means

After copying the item into the runtime hub, installPostoverflowItem creates a symlink from the runtime parser/postoverflow directory to the hub copy. If os.Symlink fails for a reason other than the link already existing, the error is wrapped with both paths. An existing symlink is tolerated (os.IsExist check).

Solutions

  1. Ensure the itemTypeDirDest directory exists before install (check earlier MkdirAll results)
  2. Remove the stale non-symlink file at the destination path and re-run
  3. On Windows, run with privileges or enable developer mode for symlink support
  4. Check the wrapped error (EACCES/ENOENT) and fix the underlying path/permission issue

Example fix

// before
if err := os.Symlink(hubDirPostoverflowPath, postoverflowDirParserPath); err != nil {
    return fmt.Errorf("unable to symlink postoverflow '%s' to '%s': %w", ...)
}
// after
_ = os.Remove(postoverflowDirParserPath)
if err := os.Symlink(hubDirPostoverflowPath, postoverflowDirParserPath); err != nil {
    return fmt.Errorf("unable to symlink postoverflow '%s' to '%s': %w", ...)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if _, err := os.Stat(filepath.Dir(linkPath)); err != nil {
    return fmt.Errorf("symlink parent missing: %w", err)
}
if fi, err := os.Lstat(linkPath); err == nil && fi.Mode()&os.ModeSymlink == 0 {
    os.Remove(linkPath) // stale non-symlink file
}

Try / catch

if err := t.installPostoverflow(item); err != nil {
    if strings.Contains(err.Error(), "unable to symlink") && runtime.GOOS == "windows" {
        return fmt.Errorf("symlinks need admin/developer mode on windows: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: os.Symlink(hubDirPostoverflowPath, postoverflowDirParserPath) fails: parent directory of the symlink target doesn't exist, permission denied, or on Windows where symlink creation requires privileges.

Common situations: Runtime postoverflow stage directory not created before symlinking; running hub tests on Windows without developer mode/privileges; stale file at the symlink destination that is not a symlink.

Understand the failure class

Background: "Permission denied" / "Failed to write" file errors: why a library can't write its files to disk (EACCES, EPERM, ENOSPC) and how to fix them — this error's family across 43 libraries.

Related errors


AI-assisted analysis of crowdsecurity/crowdsec@909b515798 (2026-09-06). Data as JSON: /api/errors/7b5b06d2fa33fc11. Report an issue: GitHub.

Appendix: source

Thrown at pkg/hubtest/postoverflow.go:40

	// runtime/postoverflows/s00-enrich
	itemTypeDirDest := fmt.Sprintf("%s/postoverflows/%s/", t.RuntimePath, item.Stage)

	if err := createDirs([]string{hubDirPostoverflowDest, itemTypeDirDest}); err != nil {
		return err
	}

	// runtime/hub/postoverflows/s00-enrich/crowdsecurity/rdns.yaml
	hubDirPostoverflowPath := filepath.Join(hubDirPostoverflowDest, sourceFilename)
	if err := Copy(sourcePath, hubDirPostoverflowPath); err != nil {
		return fmt.Errorf("unable to copy '%s' to '%s': %w", sourcePath, hubDirPostoverflowPath, err)
	}

	// runtime/postoverflows/s00-enrich/rdns.yaml
	postoverflowDirParserPath := filepath.Join(itemTypeDirDest, sourceFilename)
	if err := os.Symlink(hubDirPostoverflowPath, postoverflowDirParserPath); err != nil {
		if !os.IsExist(err) {
			return fmt.Errorf("unable to symlink postoverflow '%s' to '%s': %w", hubDirPostoverflowPath, postoverflowDirParserPath, err)
		}
	}

	return nil
}

func (t *HubTestItem) installPostoverflowCustomFrom(postoverflow string, customPath string) (bool, error) {
	// we check if its a custom postoverflow
	customPostOverflowPath := filepath.Join(customPath, postoverflow)
	if _, err := os.Stat(customPostOverflowPath); os.IsNotExist(err) {
		return false, nil
	}

	customPostOverflowPathSplit := strings.Split(customPostOverflowPath, "/")
	customPostoverflowName := customPostOverflowPathSplit[len(customPostOverflowPathSplit)-1]
	// because path is postoverflows/<stage>/<author>/parser.yaml and we wan't the stage
	customPostoverflowStage := customPostOverflowPathSplit[len(customPostOverflowPathSplit)-3]

View on GitHub (pinned to 909b515798)