vxcontrol/pentagi · error

container path '%s' is not a directory

Error message

container path '%s' is not a directory

What it means

ListContainerDir stats the requested path and rejects it when the stat result's mode is not a directory. The path exists in the container but is a regular file, symlink to a file, socket, or other non-directory entry.

Source

Thrown at backend/pkg/docker/client.go:976

	// and only the first maxListEntries were listed.
	Truncated bool
}

func (dc *dockerClient) ListContainerDir(
	ctx context.Context,
	containerID string,
	dirPath string,
) (ContainerDirListing, error) {
	if strings.TrimSpace(dirPath) == "" {
		dirPath = WorkFolderPathInContainer
	}

	dirStat, err := dc.ContainerStatPath(ctx, containerID, dirPath)
	if err != nil {
		return ContainerDirListing{}, fmt.Errorf("failed to stat container path '%s': %w", dirPath, err)
	}
	if !dirStat.Mode.IsDir() {
		return ContainerDirListing{}, fmt.Errorf("container path '%s' is not a directory", dirPath)
	}

	// List direct children NUL-delimited. Parsing `ls` output is unsafe: under a
	// TTY GNU coreutils shell-quotes names and busybox wraps them in ANSI escapes,
	// so a readable file with a space / quote / non-ASCII byte would be mis-stat'd
	// and reported unreadable. `find -print0` emits literal bytes and is portable
	// (GNU + busybox); the NUL delimiter also survives names containing newlines.
	// No TTY: a TTY's onlcr would rewrite every \n in the stream to \r\n —
	// including a \n that is part of a filename — corrupting the name. Without a
	// TTY the exec stream is multiplexed and demuxed below.
	createResp, err := dc.ContainerExecCreate(ctx, containerID, client.ExecCreateOptions{
		Cmd:          []string{"find", dirPath, "-maxdepth", "1", "-mindepth", "1", "!", "-name", ".*", "-print0"},
		AttachStdout: true,
		AttachStderr: true,
	})
	if err != nil {
		return ContainerDirListing{}, fmt.Errorf("failed to create list exec for '%s': %w", dirPath, err)
	}

View on GitHub (pinned to ea665308ba)

Solutions

  1. Pass a directory path, not a file path — strip the filename or use filepath.Dir-style handling on the container side
  2. Verify the target with `docker exec <id> stat <path>` and adjust the argument
  3. If a work folder is volume-mounted, mount a directory, not a single file
  4. Handle the error gracefully in tool code by returning the file's metadata or a clear message instead of a listing

Example fix

// before
listing, err := c.ListContainerDir(ctx, containerID, "/work/output.txt")
// after
dir := "/work/output.txt"
if strings.HasSuffix(dir, ".txt") {
    dir = "/work"
}
listing, err := c.ListContainerDir(ctx, containerID, dir)
Defensive patterns

Strategy: validation

Validate before calling

// stat first and branch on type before listing
st, err := c.ContainerStatPath(ctx, id, p)
if err == nil && !st.Mode.IsDir() {
    // caller passed a file; handle as file or use its parent directory
    p = path.Dir(p)
}

Try / catch

listing, err := c.ListContainerDir(ctx, id, p)
if err != nil && strings.Contains(err.Error(), "is not a directory") {
    return c.ListContainerDir(ctx, id, path.Dir(p)) // fall back to parent
}

Prevention

When it happens

Trigger: Caller passes a file path (e.g. /work/results.txt) or a symlink pointing at a file to ListContainerDir; empty dirPath defaulting to WorkFolderPathInContainer when that default itself is a mounted file; path case-mismatch resolving to a file.

Common situations: LLM agent passing a filename instead of a directory to a 'list directory' tool call; user mounting a single file at the work-folder path in docker-compose; symlinked 'folders' that actually point at files; Windows-style path strings that resolve oddly in Linux containers.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/5d3cd057377e41fc. Report an issue: GitHub.