kovidgoyal/kitty · error

%s is not a directory while resolving symlinks in %s

Error message

%s is not a directory while resolving symlinks in %s

What it means

Raised by EvalSymlinksThatExist while walking a path component-by-component: it encountered a non-directory entry at a position that still has remaining path segments. In other words, an intermediate component of the path exists as a regular file (or other non-dir, non-symlink node), so it cannot be descended into to resolve the rest of the path. ExtractAllFromTar hits this when a tar entry's target path routes through a file.

Source

Thrown at tools/utils/tar.go:110

		dest += path[start:end]

		// Resolve symlink.

		fi, err := os.Lstat(dest)
		if err != nil {
			if os.IsNotExist(err) {
				if end < len(path) {
					dest += path[end:]
				}
				return filepath.Clean(dest), nil
			}
			return "", err
		}

		if fi.Mode()&fs.ModeSymlink == 0 {
			if !fi.Mode().IsDir() && end < len(path) {
				return "", fmt.Errorf("%s is not a directory while resolving symlinks in %s", dest, path)
			}
			continue
		}

		// Found symlink.

		linksWalked++
		if linksWalked > 255 {
			return "", fmt.Errorf("EvalSymlinksThatExist: too many symlinks in %s", path)
		}

		link, err := os.Readlink(dest)
		if err != nil {
			return "", err
		}

		if isWindowsDot && !filepath.IsAbs(link) {
			// On Windows, if "." is a relative symlink,

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Clear or use a fresh destination directory before extraction (rm -rf the previous extraction target).
  2. Inspect the tar entry list for a name used both as file and directory; fix the archive or filter entries.
  3. Ensure the destination path itself doesn't traverse an existing regular file.
  4. If extracting programmatically, pre-create needed directories and skip/merge conflicting file entries.

Example fix

// before
err := ExtractAllFromTar(tarReader, "build/out") // build/out is an old regular file
// → "build/out is not a directory while resolving symlinks in build/out/pkg/file.go"

// after
os.RemoveAll("build/out")
os.MkdirAll("build/out", 0o755)
err := ExtractAllFromTar(tarReader, "build/out")
Defensive patterns

Strategy: validation

Validate before calling

func destIsCleanDir(dest string) bool {
	fi, err := os.Lstat(dest)
	if os.IsNotExist(err) { return true }
	return err == nil && fi.IsDir()
}

Type guard

func extractionPathClear(dest string) error {
	fi, err := os.Lstat(dest)
	if err != nil && !os.IsNotExist(err) { return err }
	if err == nil && !fi.IsDir() { return fmt.Errorf("%s exists and is not a directory", dest) }
	return nil
}

Try / catch

if err := ExtractAllFromTar(r, dest); err != nil {
	if strings.Contains(err.Error(), "is not a directory while resolving symlinks") {
		os.RemoveAll(dest)
		os.MkdirAll(dest, 0o755)
		err = ExtractAllFromTar(r, dest)
	}
	if err != nil { return err }
}

Prevention

When it happens

Trigger: ExtractAllFromTar extracting an archive containing entries like "a/b" where "a" already exists in the destination as a regular file, or where an earlier tar entry created "a" as a file and a later entry expects "a/b". Also when dest itself plus a remaining subpath crosses a file boundary.

Common situations: Extracting a tarball over a stale destination tree from a previous run (old file now blocks a directory path), archives with entries that reuse a name first as a file then as a directory, or passing a destination path whose prefix names a file (e.g. dest="out.txt/sub").

Related errors


AI-assisted analysis of kovidgoyal/kitty@6d5d0c4406 (2026-08-27). Data as JSON: /api/errors/2a4e49451d39af3e. Report an issue: GitHub.