Skip to content
Open
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
51 changes: 51 additions & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Provider names today:

- `codex`: shells out to `codex exec` (default)
- `acpx`: routes through any ACP-compatible coding agent via `acpx`
- `gemini`: shells out to Google Gemini CLI in headless mode
- `grok`: shells out to the xAI Grok Build CLI in headless mode (`grok --prompt-file`)
- `opencode`: shells out to `opencode run --format json`
- `pi`: shells out to `pi -p` (non-interactive print mode)
Expand Down Expand Up @@ -105,6 +106,56 @@ Migration note: `--provider codex --model gpt-5-codex` is not equivalent to
`--provider acpx --model gpt-5-codex`; the latter selects an ACP agent named
`gpt-5-codex`. Use `--provider acpx --model codex:gpt-5-codex`.

## Gemini

The `gemini` provider shells out to the local
[Gemini CLI](https://github.com/google-gemini/gemini-cli) in headless mode.

Install Gemini CLI and authenticate using one of the upstream-supported flows:

```bash
npm install -g @google/gemini-cli
gemini --version
```

Provider selection:

```bash
CLAWPATCH_GEMINI_TRUST_WORKSPACE=true clawpatch review --provider gemini
CLAWPATCH_GEMINI_TRUST_WORKSPACE=true clawpatch fix --finding <id> --provider gemini
CLAWPATCH_GEMINI_TRUST_WORKSPACE=true clawpatch doctor --provider gemini
```

How the Gemini provider works:

- Headless mode: `gemini --skip-trust -p "" --approval-mode=<mode> --output-format=json`
- Prompt delivery: Clawpatch writes the full prompt to stdin; it does not pass large prompts on argv
- Read-only operations (map, review, revalidate): use `--approval-mode=plan`
- Write operations (fix): use `--approval-mode=auto_edit`, not yolo mode
- Output: parses Gemini's JSON envelope and extracts the string `response` field before validating Clawpatch JSON
- Model selection: `--model <model>` is passed through when configured
- Reasoning effort and `skipGitRepoCheck`: not supported by Gemini CLI and are treated as no-ops
- Timeout: 180 seconds by default, override with `CLAWPATCH_GEMINI_TIMEOUT_MS` or `CLAWPATCH_PROVIDER_TIMEOUT_MS`

Security gates:

- Gemini CLI must be patched for GHSA-wpqr-6v78-jr5g. Clawpatch accepts
stable versions `>=0.39.1` and preview versions `>=0.40.0-preview.3`.
Set `CLAWPATCH_GEMINI_ALLOW_UNPATCHED=1` only for local diagnostics.
- Clawpatch uses `--skip-trust` because Gemini headless execution requires an
explicit trusted-workspace path. You must opt in with
`CLAWPATCH_GEMINI_TRUST_WORKSPACE=true`; use this only in an isolated checkout
with no untrusted project Gemini configuration or secrets.
- Gemini subprocesses run with isolated temp `HOME` and XDG dirs. Clawpatch
copies only the minimal verified Gemini auth/config files into that temp home,
and forwards a small env allowlist: path/temp basics, explicit Google/Gemini
auth variables, proxy and certificate vars, and `NO_COLOR`. Wildcard secret
prefixes are not forwarded.
- Clawpatch passes `--extensions none` and prompts read-only operations not to use
network, MCP, skills, subagents, shell, or write tools. Enforcement still
depends on Gemini CLI policy behavior, so review untrusted code in an isolated
checkout.

## Grok

The `grok` provider shells out to the local [Grok Build CLI](https://x.ai/cli).
Expand Down
7 changes: 3 additions & 4 deletions docs/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ Current safety rules:
- `fix` refuses a dirty source worktree by default.
- `.clawpatch/` state changes are allowed during runs.
- review and revalidate provider calls use a read-only sandbox for the `codex`
provider. The `acpx` provider relies on `acpx --approve-reads` plus an explicit
read-only prompt directive; underlying agents that bypass ACP permissions (e.g.
agents running in their own full-access mode) may not be strictly sandboxed.
See docs/providers.md.
provider. Other local CLI providers rely on their own approval modes and
explicit read-only prompt directives; underlying agents may not be strictly
sandboxed. See docs/providers.md.
- provider output must pass runtime schema validation.
- feature locks are stored in feature records and `.clawpatch/locks/`; `status`
surfaces both, and `clean-locks` clears both.
Expand Down
14 changes: 12 additions & 2 deletions src/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,23 @@ export async function runCommandArgs(
args: string[],
cwd: string,
input?: string,
options: { trimOutput?: boolean; env?: NodeJS.ProcessEnv; timeoutMs?: number } = {},
options: {
trimOutput?: boolean;
env?: NodeJS.ProcessEnv;
timeoutMs?: number;
inheritEnv?: boolean;
} = {},
): Promise<CommandResult> {
const started = Date.now();
const spawnSpec = commandSpawnSpec(program, args);
const child = spawn(spawnSpec.program, spawnSpec.args, {
cwd,
env: options.env === undefined ? process.env : { ...process.env, ...options.env },
env:
options.env === undefined
? process.env
: options.inheritEnv === false
? options.env
: { ...process.env, ...options.env },
detached: process.platform !== "win32" && options.timeoutMs !== undefined,
shell: false,
stdio: ["pipe", "pipe", "pipe"],
Expand Down
12 changes: 5 additions & 7 deletions src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,11 @@ export async function buildReviewPromptBundle(
];
const fileBlocks: string[] = [];
const includedFiles: ReviewPromptFileManifest[] = [];
for (const ref of owned) {
const file = await fileBlockWithManifest(root, ref.path, "owned");
fileBlocks.push(file.block);
includedFiles.push(file.manifest);
}
for (const ref of context) {
const file = await fileBlockWithManifest(root, ref.path, "context");
const files = await Promise.all([
...owned.map((ref) => fileBlockWithManifest(root, ref.path, "owned")),
...context.map((ref) => fileBlockWithManifest(root, ref.path, "context")),
]);
for (const file of files) {
fileBlocks.push(file.block);
includedFiles.push(file.manifest);
}
Expand Down
20 changes: 18 additions & 2 deletions src/provider-json.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { ClawpatchError } from "./errors.js";

const MAX_FALLBACK_JSON_SCAN_BYTES = 4 * 1024 * 1024;
const MAX_FALLBACK_JSON_DEPTH = 1_000;
const MAX_FALLBACK_JSON_CANDIDATES = 64;

export function extractJson(text: string): unknown | null {
try {
return JSON.parse(text);
} catch {}
if (Buffer.byteLength(text, "utf8") > MAX_FALLBACK_JSON_SCAN_BYTES) {
return null;
}
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/u);
if (fenceMatch && fenceMatch[1]) {
const candidate = fenceMatch[1].trim();
Expand All @@ -12,7 +19,12 @@ export function extractJson(text: string): unknown | null {
} catch {}
}
let firstBrace = text.indexOf("{");
let candidates = 0;
while (firstBrace !== -1) {
candidates += 1;
if (candidates > MAX_FALLBACK_JSON_CANDIDATES) {
return null;
}
let depth = 0;
let inString = false;
let escape = false;
Expand All @@ -31,8 +43,12 @@ export function extractJson(text: string): unknown | null {
continue;
}
if (!inString) {
if (ch === "{") depth += 1;
else if (ch === "}") {
if (ch === "{") {
depth += 1;
if (depth > MAX_FALLBACK_JSON_DEPTH) {
return null;
}
} else if (ch === "}") {
depth -= 1;
if (depth === 0) {
const candidate = text.slice(firstBrace, i + 1);
Expand Down
Loading