diff --git a/package-lock.json b/package-lock.json index a327f32..29b9551 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "clarp-cli", - "version": "0.1.12", + "version": "0.1.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "clarp-cli", - "version": "0.1.12", + "version": "0.1.13", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index edee2b7..ddec755 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clarp-cli", - "version": "0.1.12", + "version": "0.1.13", "description": "Drop-in replacement for claude -p. Uses your Claude Code subscription instead of metered API pricing.", "type": "module", "author": "dn00", diff --git a/src/claude-prompts.test.ts b/src/claude-prompts.test.ts index a2217c4..e7235d8 100644 --- a/src/claude-prompts.test.ts +++ b/src/claude-prompts.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { - createWorkspaceTrustPromptDetector, + createStartupPromptDetector, + isBypassPermissionsPrompt, isWorkspaceTrustPrompt, shouldAutoConfirmWorkspaceTrust, stripTerminalControls, @@ -21,6 +22,19 @@ Accessing workspace: Enter to confirm · Esc to cancel `; +const BYPASS_PROMPT = ` + WARNING: Claude Code running in Bypass Permissions mode + + In Bypass Permissions mode, Claude Code will not ask for your approval before running potentially dangerous commands. + + By proceeding, you accept all responsibility for actions taken while running in Bypass Permissions mode. + + ❯ 1. No, exit + 2. Yes, I accept + + Enter to confirm · Esc to cancel +`; + describe("Claude prompt detection", () => { it("strips terminal controls", () => { expect(stripTerminalControls("\x1b[32mhello\x1b[0m\rworld")).toBe("hello\nworld"); @@ -45,6 +59,12 @@ describe("Claude prompt detection", () => { expect(isWorkspaceTrustPrompt("Hi! What can I help you with?")).toBe(false); }); + it("detects the bypass-permissions warning prompt", () => { + expect(isBypassPermissionsPrompt(BYPASS_PROMPT)).toBe(true); + expect(isBypassPermissionsPrompt(TRUST_PROMPT)).toBe(false); + expect(isBypassPermissionsPrompt("Hi! What can I help you with?")).toBe(false); + }); + it("auto-confirms workspace trust only for explicit permission bypass", () => { expect(shouldAutoConfirmWorkspaceTrust(["--dangerously-skip-permissions"])).toBe(true); expect(shouldAutoConfirmWorkspaceTrust(["--permission-mode", "bypassPermissions"])).toBe(false); @@ -53,7 +73,7 @@ describe("Claude prompt detection", () => { it("detects prompts split across PTY chunks only once", () => { const onDetected = vi.fn(); - const detector = createWorkspaceTrustPromptDetector(onDetected); + const detector = createStartupPromptDetector(onDetected); detector(TRUST_PROMPT.slice(0, 120)); expect(onDetected).not.toHaveBeenCalled(); @@ -62,5 +82,19 @@ describe("Claude prompt detection", () => { detector(TRUST_PROMPT); expect(onDetected).toHaveBeenCalledTimes(1); + expect(onDetected).toHaveBeenCalledWith("trust"); + }); + + it("reports trust then bypass, each once, as the dialogs stream in", () => { + const onDetected = vi.fn(); + const detector = createStartupPromptDetector(onDetected); + + detector(TRUST_PROMPT); + detector(BYPASS_PROMPT); + detector(BYPASS_PROMPT); + + expect(onDetected).toHaveBeenNthCalledWith(1, "trust"); + expect(onDetected).toHaveBeenNthCalledWith(2, "bypass"); + expect(onDetected).toHaveBeenCalledTimes(2); }); }); diff --git a/src/claude-prompts.ts b/src/claude-prompts.ts index 7737c8c..c69476f 100644 --- a/src/claude-prompts.ts +++ b/src/claude-prompts.ts @@ -11,20 +11,36 @@ export function isWorkspaceTrustPrompt(text: string): boolean { && compact.includes("noexit"); } +/** Second startup dialog (only with --dangerously-skip-permissions); its default option is "No, exit", so a bare Enter quits. */ +export function isBypassPermissionsPrompt(text: string): boolean { + const compact = stripTerminalControls(text).toLowerCase().replace(/[^a-z0-9]/g, ""); + return compact.includes("bypasspermissionsmode") + && compact.includes("yesiaccept") + && compact.includes("noexit"); +} + export function shouldAutoConfirmWorkspaceTrust(claudeArgs: string[]): boolean { return claudeArgs.includes("--dangerously-skip-permissions"); } -export function createWorkspaceTrustPromptDetector(onDetected: () => void): (data: string) => void { +export type StartupPrompt = "trust" | "bypass"; + +/** Reports each first-run startup dialog (workspace trust, then bypass-permissions warning) at most once. */ +export function createStartupPromptDetector( + onDetected: (prompt: StartupPrompt) => void, +): (data: string) => void { let buffer = ""; - let detected = false; + const fired = new Set(); return (data: string) => { - if (detected) return; buffer = (buffer + data).slice(-16_000); - if (isWorkspaceTrustPrompt(buffer)) { - detected = true; - onDetected(); + if (!fired.has("trust") && isWorkspaceTrustPrompt(buffer)) { + fired.add("trust"); + onDetected("trust"); + } + if (!fired.has("bypass") && isBypassPermissionsPrompt(buffer)) { + fired.add("bypass"); + onDetected("bypass"); } }; } diff --git a/src/cli.ts b/src/cli.ts index a02c9e9..a4add83 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,7 +3,7 @@ import { parseArgs } from "./args.js"; import { ProxyBackend } from "./backends/proxy-backend.js"; import { - createWorkspaceTrustPromptDetector, + createStartupPromptDetector, shouldAutoConfirmWorkspaceTrust, stripTerminalControls, } from "./claude-prompts.js"; @@ -59,7 +59,13 @@ async function main(): Promise { const shouldAutoTrustWorkspace = shouldAutoConfirmWorkspaceTrust(args.claudeArgs); log(`Spawning: claude ${claudeArgs.join(" ")}`); - const detectWorkspaceTrustPrompt = createWorkspaceTrustPromptDetector(() => { + const detectStartupPrompt = createStartupPromptDetector((prompt) => { + if (prompt === "bypass") { + // Default-highlighted option is "No, exit"; pick option "2. Yes, I accept" by number then confirm. + log("Auto-accepting Claude Bypass Permissions mode warning because --dangerously-skip-permissions was passed."); + ptyHandle?.write("2\r"); + return; + } trustPromptDetected = true; if (shouldAutoTrustWorkspace) { log("Auto-confirming Claude workspace trust prompt because --dangerously-skip-permissions was passed."); @@ -90,7 +96,7 @@ async function main(): Promise { const text = stripTerminalControls(data).trim(); if (text) process.stderr.write(`[clarp] Claude PTY: ${text}\n`); } - detectWorkspaceTrustPrompt(data); + detectStartupPrompt(data); }, onExit: (code: number) => { fatalCleanup?.markChildExited();