JuliusBrussee/caveman · warning · Error

caveman-code: not a file: ${input.path}

Error message

caveman-code: not a file: ${input.path}

What it means

Thrown by the caveman-code `read_file` tool. The requested path was first checked for workspace containment (realpath-based), then `stat`ed; `info.isFile()` is false, meaning the path exists (or a contained symlink resolves) to a directory, FIFO, socket, device, or other non-regular file. Only regular files can be read, so the tool fails with the model-supplied path echoed back.

Source

Thrown at packages/agent/src/code.ts:242

    containedPath(await workspaceRoot(), candidate);

  const readFileTool = tool({
    name: "read_file",
    description:
      "Read a UTF-8 file from the workspace. Optional offset/limit read a line range. " +
      `Output is capped at ${caps.read_file} bytes.`,
    input: schema.object({
      path: schema.string(),
      offset: schema.optional(schema.integer()),
      limit: schema.optional(schema.integer()),
    }),
    effect: "read",
    result: "inline",
    timeoutMs: READ_TIMEOUT_MS,
    async execute(input) {
      const target = await contained(input.path);
      const info = await stat(target);
      if (!info.isFile()) throw new Error(`caveman-code: not a file: ${input.path}`);
      const content = await readFile(target, "utf8");
      const lines = content.split("\n");
      const offset = Math.max(1, input.offset ?? 1);
      const limit = input.limit === undefined ? lines.length : Math.max(1, input.limit);
      const selected = lines.slice(offset - 1, offset - 1 + limit);
      const numbered = selected
        .map((line, index) => `${offset + index}\t${line}`)
        .join("\n");
      const text = capOutput(numbered, caps.read_file);
      record(`read_file:${input.path}`, text);
      return text;
    },
  });

  const grepTool = tool({
    name: "grep",
    description:
      "Search the workspace for a regular expression with ripgrep (grep fallback). " +

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Instruct the model (system prompt/tool docs) to use the listing/grep tool for directories and read_file only for regular files.
  2. Pass an explicit file path with its extension (`src/index.ts`, not `src`).
  3. If a tool call must not fail the turn, catch this error in the turn loop and let the model retry with a corrected path.

Example fix

// before: model calls the tool with a directory
tools.read_file({ path: "src" });

// after
tools.read_file({ path: "src/index.ts", offset: 1, limit: 50 });
Defensive patterns

Strategy: type-guard

Validate before calling

import { stat } from "node:fs/promises";

async function isRegularFile(path: string): Promise<boolean> {
  try { return (await stat(path)).isFile(); } catch { return false; }
}
// model-side guard before calling read_file: await isRegularFile(resolvedPath)

Type guard

import type { Stats } from "node:fs";
function isRegularFile(info: Stats): boolean {
  return info.isFile();
}

Try / catch

// inside a turn loop driving the coding agent:
try {
  return await tools.read_file.execute({ path });
} catch (error) {
  if (error instanceof Error && error.message.startsWith("caveman-code: not a file:")) {
    return `Error: ${path} is not a regular file. Use the listing or grep tool for directories.`; // let the model correct itself
  }
  throw error;
}

Prevention

When it happens

Trigger: A coding-agent model calls `read_file` with a directory path (e.g. `"src"` instead of `"src/index.ts"`), or with `/dev/null`, a named pipe, or a socket path inside the workspace. Containment already passed, so this is purely a not-a-regular-file rejection, not a security error.

Common situations: Models exploring an unfamiliar tree guessing file names and hitting directories; attempts to read `/proc` or `/dev` entries that happen to be symlinked into the workspace; using read_file where list/grep was intended.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/9b096beba3a286ad. Report an issue: GitHub.