{"record":{"id":"b929eae610b0e131","repo":"twpayne/chezmoi","slug":"s-s-w","errorCode":null,"errorMessage":"%s: %s: %w","messagePattern":"(.+?): (.+?): %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/chezmoi/sourcestate.go","lineNumber":1463,"sourceCode":"\t\treturn fmt.Errorf(\"%s: %w\", sourceAbsPath, err)\n\t}\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\tfor targetPath, external := range externals {\n\t\tif external.TargetPath != \"\" {\n\t\t\ttargetPath = external.TargetPath\n\t\t}\n\t\tif targetPath == \"\" {\n\t\t\treturn fmt.Errorf(\"%s: empty path\", sourceAbsPath)\n\t\t}\n\n\t\texternalPath := path.Clean(targetPath)\n\t\tif strings.HasPrefix(externalPath, \"/\") || filepath.IsAbs(externalPath) {\n\t\t\treturn fmt.Errorf(\"%s: %s: path is not relative\", sourceAbsPath, targetPath)\n\t\t}\n\t\tswitch relPath, err := filepath.Rel(\".\", externalPath); {\n\t\tcase err != nil:\n\t\t\treturn fmt.Errorf(\"%s: %s: %w\", sourceAbsPath, targetPath, err)\n\t\tcase relPath == \".\":\n\t\t\treturn fmt.Errorf(\"%s: %s: empty relative path\", sourceAbsPath, targetPath)\n\t\tcase relPath == \"..\", strings.HasPrefix(relPath, \"../\"),\n\t\t\truntime.GOOS == \"windows\" && strings.HasPrefix(relPath, \"..\"+string(filepath.Separator)):\n\t\t\treturn fmt.Errorf(\"%s: %s: relative path outside target directory\", sourceAbsPath, targetPath)\n\t\t}\n\t\ttargetRelPath := parentTargetSourceRelPath.JoinString(externalPath)\n\t\texternal.sourceAbsPath = sourceAbsPath\n\t\ts.externals[targetRelPath] = append(s.externals[targetRelPath], &external)\n\t}\n\treturn nil\n}\n\n// addExternalDir adds all externals in externalsDirAbsPath to s.\nfunc (s *SourceState) addExternalDir(ctx context.Context, externalsDirAbsPath AbsPath) error {\n\twalkFunc := func(ctx context.Context, externalAbsPath AbsPath, fileInfo fs.FileInfo, err error) error {\n\t\tif externalAbsPath == externalsDirAbsPath {\n\t\t\treturn nil","sourceCodeStart":1445,"sourceCodeEnd":1481,"githubUrl":"https://github.com/twpayne/chezmoi/blob/f901167e4685db90da56d6a2a19df642cb3e0247/internal/chezmoi/sourcestate.go#L1445-L1481","documentation":"After confirming the external path is relative, chezmoi computes filepath.Rel('.', externalPath). If that computation itself fails (malformed path for the OS), the raw error is wrapped with both the external source file and the target path. This precedes the explicit 'outside target directory' and 'empty relative path' checks.","triggerScenarios":"filepath.Rel(\".\", externalPath) returning an error, e.g. paths that cannot be made relative on the current OS (volume-relative Windows paths like 'C:tool', malformed path syntax).","commonSituations":"Windows-specific path forms ('C:foo' drive-relative paths) in externals; externally generated external configs with OS-invalid path strings.","solutions":["Use a simple forward-slash relative path like 'dir/name' for the external target path.","Remove drive-letter or drive-relative components from the path.","Test the config on the target OS with 'chezmoi apply --dry-run' to catch OS-specific path issues."],"exampleFix":"// before (Windows drive-relative)\n[\"C:tools\\\\tool\"]\ntype = \"archive\"\nurl = \"https://example.com/tool.zip\"\n// after\n[\"tools/tool\"]\ntype = \"archive\"\nurl = \"https://example.com/tool.zip\"","handlingStrategy":"validation","validationCode":"if _, err := filepath.Rel(\".\", path.Clean(target)); err != nil {\n  return fmt.Errorf(\"external path %q is not a valid relative path: %w\", target, err)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Use simple POSIX-style relative paths in externals","Avoid drive-relative Windows paths like C:foo","Test externals on each target OS before rolling out"],"tags":["chezmoi","external","path-validation","windows"],"backgroundTag":"invalid-relative-path","analyzedSha":"f901167e4685db90da56d6a2a19df642cb3e0247","analyzedAt":"2026-09-01T18:16:41.508Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-09T01:17:15.007Z"}