Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ harnesses agents sync --check # doctor: link global AGENTS.md files to one mas
harnesses mcp # run the MCP server over stdio
```

`tools` defaults to `false` in the library and CLI. The MCP, Pi, and OMP tools require agents to choose it explicitly. `false` must use a native CLI flag that removes tools from the model context; it is a lightweight advisor, not an agent constrained only by prompt wording. Set `tools: true` (or CLI `--tools`) whenever the task needs harness tools, including Grok's native X search. Add `readOnly: true` when those tools must stay inside a sandbox enforced by the harness CLI; the agent tools pass it beside `tools: true`, while the library and CLI let it imply tools. Read-only mode is rejected when a harness has no verified native recipe, so it never falls back to broader access. A recipe can also carry the lowest CLI version whose enforcement was verified, and `invoke()` rejects read-only runs on older or unknown versions: Grok runs `--sandbox read-only` from 1.0.13. Harnesses whose CLI cannot disable tools reject advisor mode instead of silently running an agent and return an explicit `tools` retry when their full agent mode can handle the request.
`tools` defaults to `false` in the library and CLI. The MCP, Pi, and OMP tools require agents to choose it explicitly. `false` must use a native CLI flag that removes tools from the model context; it is a lightweight advisor, not an agent constrained only by prompt wording. Set `tools: true` (or CLI `--tools`) whenever the task needs harness tools, including Grok's native X search. Add `readOnly: true` when those tools must stay inside a sandbox enforced by the harness CLI; the agent tools pass it beside `tools: true`, while the library and CLI let it imply tools. Read-only mode is rejected when a harness has no verified native recipe, so it never falls back to broader access. A recipe can also carry the lowest CLI version whose enforcement was verified, and `invoke()` rejects read-only runs on older or unknown versions: Grok runs `--sandbox read-only` from 1.0.13, and Claude Code keeps read-only runs on `Read`, `Glob` and `Grep` with `--strict-mcp-config` from 2.1.175. Harnesses whose CLI cannot disable tools reject advisor mode instead of silently running an agent and return an explicit `tools` retry when their full agent mode can handle the request.

## How harnesses compares to unagent

Expand Down
23 changes: 20 additions & 3 deletions docs/app/data/harnesses.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,18 +226,35 @@
"--tools",
""
],
"readOnlyArgs": [
"-p",
"{prompt}",
"--strict-mcp-config",
"--tools",
"Read,Glob,Grep"
],
"readOnlyJsonArgs": [
"-p",
"--output-format",
"json",
"{prompt}",
"--strict-mcp-config",
"--tools",
"Read,Glob,Grep"
],
"readOnlyMinVersion": "2.1.175",
"modelArgs": [
"--model",
"{model}"
],
"level": "official",
"note": "Headless print mode; add --output-format json for structured output."
"note": "Headless print mode; add --output-format json for structured output. Read-only runs keep the built-in Read, Glob and Grep tools and --strict-mcp-config drops every configured MCP server, so no write tool exists whatever permission mode the settings carry; verified on 2.1.175 and 2.1.268. --permission-mode plan does not qualify: its shell commands run through the auto mode classifier, which let a file write through."
},
"invocationModes": {
"advisor": true,
"advisorStructured": true,
"readOnly": false,
"readOnlyStructured": false,
"readOnly": true,
"readOnlyStructured": true,
"agent": true,
"agentStructured": true
},
Expand Down
2 changes: 1 addition & 1 deletion docs/content/1.guide/03.invoke.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ result.exitCode; // number, or null when stopped by a timeout or a signal
`readOnly` implies `tools`. A harness that lacks the template for the requested mode rejects the call and says which retry would work:

```
Harness claude has no read-only full agent invocation
Harness gemini has no read-only full agent invocation
Harness codex has no advisor without tools invocation; retry with tools: true to start its full agent
```

Expand Down
2 changes: 1 addition & 1 deletion docs/content/1.guide/10.explorer.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Nothing is spawned and nothing is read from your machine. One liberty: on Window

```bash
harnesses paths claude
harnesses run claude --read-only "…" # rejected, and the explorer shows you why before you try
harnesses run gemini --read-only "…" # rejected, and the explorer shows you why before you try
```

Every state of the page is in the URL, so a link to `/explorer?id=codex&platform=win32&mode=readOnly` opens exactly that. "Where does Grok keep sessions on a Mac" is a question a page answers faster than a file in `src/harnesses/`, and that's the whole reason it exists.
6 changes: 3 additions & 3 deletions docs/content/2.harnesses/00.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ Each page is generated from the same registry the package ships, so a path here
| Harness | ID | Detection | Headless | Sessions |
| --- | --- | --- | --- | --- |
| [Antigravity CLI](/harnesses/antigravity) | `antigravity` | project | agent | Protocol Buffers, JSONL, SQLite |
| [Claude Code](/harnesses/claude) | `claude` | env + project | advisor, agent | JSONL |
| [Claude Code](/harnesses/claude) | `claude` | env + project | advisor, read-only, agent | JSONL |
| [Codex CLI](/harnesses/codex) | `codex` | project | read-only, agent | JSONL, SQLite |
| [Cursor](/harnesses/cursor) | `cursor` | env + project | agent | - |
| [Freebuff](/harnesses/freebuff) | `freebuff` | project | - | JSON, JSONL |
| [Gemini CLI](/harnesses/gemini) | `gemini` | env + project | agent | JSON |
| [GitHub Copilot](/harnesses/github-copilot) | `github-copilot` | env + project | agent | - |
| [Grok CLI](/harnesses/grok) | `grok` | env + project | agent | JSONL, JSON, SQLite |
| [Grok CLI](/harnesses/grok) | `grok` | env + project | read-only, agent | JSONL, JSON, SQLite |
| [Mastra Code](/harnesses/mastracode) | `mastracode` | project | - | SQLite |
| [OMP](/harnesses/omp) | `omp` | env + project | agent | JSONL, SQLite |
| [OpenCode](/harnesses/opencode) | `opencode` | project | agent | SQLite |
| [Pi](/harnesses/pi) | `pi` | env + project | advisor, read-only, agent | JSONL |

"Headless" lists the mode families `invoke()` can run; each has a structured variant where the CLI has a JSON output flag. Read-only means a sandbox the CLI enforces itself, which today is Codex's `--sandbox read-only` and Pi's tool allowlist. Copilot is the one harness with an agent mode and no structured variant.
"Headless" lists the mode families `invoke()` can run; each has a structured variant where the CLI has a JSON output flag. Read-only means the CLI itself keeps the run from writing: Codex and Grok through `--sandbox read-only`, Pi and Claude Code through a tool allowlist. Copilot is the one harness with an agent mode and no structured variant.

## Audio and video

Expand Down
2 changes: 1 addition & 1 deletion docs/content/2.harnesses/01.claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ description: Anthropic's CLI. Headless -p mode with a real advisor, JSONL transc

Claude Code is the harness most of the others borrow from: Cursor, Copilot, OpenCode and Mastra Code read `.claude/skills/`, Grok loads `CLAUDE.md`. Its own layout is settings in JSON at user, project, local and managed scopes, transcripts under `~/.claude/projects/` in a directory named after the dash-encoded working directory, and MCP servers in `~/.claude.json` rather than in the settings file, which surprises everyone once.

`--tools ""` removes every tool from the model, so the advisor mode is real. There is no native read-only sandbox, so `readOnly: true` is rejected rather than approximated with a permission prompt you'd never see in headless mode.
`--tools ""` removes every tool from the model, so the advisor mode is real. `--tools Read,Glob,Grep` with `--strict-mcp-config` is the read-only mode: three inspection tools and no MCP servers, so nothing is left that writes. Plan mode is not it. It refuses Write and MCP calls, but a shell redirect went through the auto mode classifier and landed on disk.

::harness-sheet{id="claude"}
::
13 changes: 12 additions & 1 deletion src/harnesses/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,20 @@ export default class Claude extends Harness {
jsonArgs: ["-p", "--output-format", "json", "{prompt}"],
noToolsArgs: ["-p", "{prompt}", "--tools", ""],
noToolsJsonArgs: ["-p", "--output-format", "json", "{prompt}", "--tools", ""],
readOnlyArgs: ["-p", "{prompt}", "--strict-mcp-config", "--tools", "Read,Glob,Grep"],
readOnlyJsonArgs: [
"-p",
"--output-format",
"json",
"{prompt}",
"--strict-mcp-config",
"--tools",
"Read,Glob,Grep",
],
readOnlyMinVersion: "2.1.175",
modelArgs: ["--model", "{model}"],
level: "official",
note: "Headless print mode; add --output-format json for structured output.",
note: "Headless print mode; add --output-format json for structured output. Read-only runs keep the built-in Read, Glob and Grep tools and --strict-mcp-config drops every configured MCP server, so no write tool exists whatever permission mode the settings carry; verified on 2.1.175 and 2.1.268. --permission-mode plan does not qualify: its shell commands run through the auto mode classifier, which let a file write through.",
};
override readonly mcpConfigs: Harness["mcpConfigs"] = [
{
Expand Down
42 changes: 38 additions & 4 deletions test/invoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,40 @@ describe("normalized invocation", () => {
});
});

it("keeps Claude read-only runs on its built-in inspection tools", () => {
const claude = getHarness("claude");

expect(claude.invocation?.readOnlyMinVersion).toBe("2.1.175");
expect(claude.buildInvocation("review this", { readOnly: true })).toEqual({
command: "claude",
args: ["-p", "review this", "--strict-mcp-config", "--tools", "Read,Glob,Grep"],
});
expect(claude.buildInvocation("review this", { readOnly: true, structured: true })).toEqual({
command: "claude",
args: [
"-p",
"--output-format",
"json",
"review this",
"--strict-mcp-config",
"--tools",
"Read,Glob,Grep",
],
});
expect(claude.buildInvocation("review this", { readOnly: true, model: "sonnet" })).toEqual({
command: "claude",
args: [
"-p",
"review this",
"--strict-mcp-config",
"--tools",
"Read,Glob,Grep",
"--model",
"sonnet",
],
});
});

it("uses the full agent invocation only when tools are enabled", () => {
const claude = getHarness("claude");
expect(claude.buildInvocation("do the thing", { tools: true })).toEqual({
Expand Down Expand Up @@ -176,7 +210,7 @@ describe("normalized invocation", () => {
});

it("rejects read-only access when a harness cannot enforce it", () => {
expect(getHarness("claude").invocationError({ readOnly: true })).toContain(
expect(getHarness("gemini").invocationError({ readOnly: true })).toContain(
"no read-only full agent invocation",
);
});
Expand Down Expand Up @@ -483,8 +517,8 @@ describe("harness metadata for agents", () => {
expect(getHarness("claude").invocationModes).toEqual({
advisor: true,
advisorStructured: true,
readOnly: false,
readOnlyStructured: false,
readOnly: true,
readOnlyStructured: true,
agent: true,
agentStructured: true,
});
Expand Down Expand Up @@ -721,7 +755,7 @@ describe("runHarness tool operation", () => {
});

it("keeps unsupported read-only access from widening to a full agent", async () => {
const result = await runHarness("claude", "inspect", { tools: true, readOnly: true });
const result = await runHarness("omp", "inspect", { tools: true, readOnly: true });

expect(result.isError).toBe(true);
expect(result.content[0]?.text).toContain("no read-only full agent invocation");
Expand Down
Loading