crowdsecurity/crowdsec · error

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

Error message

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

What it means

After copying the scenario into the runtime hub directory, installScenarioItem creates a symlink from the item-type directory (runtime/scenarios/<name>.yaml) to the hub copy. If os.Symlink fails for any reason other than the link already existing, this error wraps it. Existing symlinks are intentionally tolerated.

Source

Thrown at pkg/hubtest/scenario.go:39

	// runtime/parsers/scenarios/
	itemTypeDirDest := fmt.Sprintf("%s/scenarios/", t.RuntimePath)

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

	// runtime/hub/scenarios/crowdsecurity/ssh-bf.yaml
	hubDirScenarioPath := filepath.Join(hubDirScenarioDest, sourceFilename)
	if err := Copy(sourcePath, hubDirScenarioPath); err != nil {
		return fmt.Errorf("unable to copy '%s' to '%s': %w", sourcePath, hubDirScenarioPath, err)
	}

	// runtime/scenarios/ssh-bf.yaml
	scenarioDirParserPath := filepath.Join(itemTypeDirDest, sourceFilename)
	if err := os.Symlink(hubDirScenarioPath, scenarioDirParserPath); err != nil {
		if !os.IsExist(err) {
			return fmt.Errorf("unable to symlink scenario '%s' to '%s': %w", hubDirScenarioPath, scenarioDirParserPath, err)
		}
	}

	return nil
}

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

	itemTypeDirDest := fmt.Sprintf("%s/scenarios/", t.RuntimePath)
	if err := os.MkdirAll(itemTypeDirDest, os.ModePerm); err != nil {
		return false, fmt.Errorf("unable to create folder '%s': %w", itemTypeDirDest, err)
	}

View on GitHub (pinned to 909b515798)

Solutions

  1. Remove the stale non-symlink file at the destination path and re-run.
  2. On Windows, run with symlink privileges (developer mode/admin) or on a filesystem supporting symlinks.
  3. Check permissions on the runtime scenarios directory.
  4. Clean the test runtime directory (rm -rf) and re-run the tests.

Example fix

// before
os.WriteFile("/runtime/scenarios/ssh-bf.yaml", data, 0644) // later install tries to symlink same path
// after
os.RemoveAll("/runtime/scenarios/ssh-bf.yaml") // then os.Symlink succeeds
Defensive patterns

Strategy: validation

Validate before calling

if fi, err := os.Lstat(dest); err == nil && fi.Mode()&os.ModeSymlink == 0 {
	os.Remove(dest) // clear stale non-symlink before install
}

Try / catch

if err := os.Symlink(target, dest); err != nil && !os.IsExist(err) {
	var le *os.LinkError
	if errors.As(err, &le) {
		return fmt.Errorf("symlink unsupported or blocked at %s: %w", dest, err)
	}
	return err
}

Prevention

When it happens

Trigger: installScenario -> installScenarioItem where a symlink must be created at runtime/scenarios/<file> but fails: the destination path exists as a regular file/directory, or the filesystem doesn't support symlinks, or permission denied.

Common situations: A previous test run left a real file (not a symlink) at the destination; running on Windows without symlink privileges; read-only runtime directory.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


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