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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
38 changes: 36 additions & 2 deletions src/claude-prompts.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import {
createWorkspaceTrustPromptDetector,
createStartupPromptDetector,
isBypassPermissionsPrompt,
isWorkspaceTrustPrompt,
shouldAutoConfirmWorkspaceTrust,
stripTerminalControls,
Expand All @@ -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");
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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);
});
});
28 changes: 22 additions & 6 deletions src/claude-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StartupPrompt>();

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");
}
};
}
12 changes: 9 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -59,7 +59,13 @@ async function main(): Promise<void> {
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.");
Expand Down Expand Up @@ -90,7 +96,7 @@ async function main(): Promise<void> {
const text = stripTerminalControls(data).trim();
if (text) process.stderr.write(`[clarp] Claude PTY: ${text}\n`);
}
detectWorkspaceTrustPrompt(data);
detectStartupPrompt(data);
},
onExit: (code: number) => {
fatalCleanup?.markChildExited();
Expand Down