diff --git a/README.md b/README.md index 9a9932c..e10ee86 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,25 @@ bro ``` 1. Scroll to a **provider** and press enter. -2. Scroll to a **model** and press enter. Press **Tab** to flip the **Skip permissions** toggle (`--dangerously-skip-permissions`) on/off right there. +2. Scroll to a **model** and press enter. Press **Tab** to flip the **Skip permissions** toggle right there — it opts this launch into the dangerous `--dangerously-skip-permissions` bypass. 3. First time on a paid provider it asks for an API key and saves it. Your last provider + model are remembered and pre-selected next time (per provider). +### Permission mode + +By default `bro` starts Claude Code in **auto mode** (`--permission-mode auto`) — +Claude's intelligent auto-mode approves safe actions and only prompts when it +needs to. Set `permissionMode` in `~/.bro/config.json` to change the default: + +- `"auto"` — auto-mode (the default) +- `"manual"` — prompt for everything (same as `bro --safe`) +- `"bypass"` — skip every permission check (`--dangerously-skip-permissions`) + +The **Skip permissions** toggle in the menu opts a single launch into `bypass`. +(The older `dangerouslySkipPermissions: true/false` config key still works when +`permissionMode` is unset.) + ## Multiple Claude Account Proxy The **top** option in the menu (`bro -p pool`) pools any number of Claude Max / Team logins behind one local endpoint and launches Claude Code across all of them — so a single session draws from several plans and **fails over automatically** the moment one runs out of usage. @@ -75,7 +89,7 @@ bro -p sakana -m fugu # skip the menus bro --list # list every provider + model bro update # refresh the model list from GitHub, cache it locally bro --dry-run # show what would run, launch nothing -bro --safe # don't pass --dangerously-skip-permissions +bro --safe # start in manual mode (prompt for everything) bro --resume # pick provider/model, then resume Claude there bro -p pool --resume # resume through the Multiple Claude Account Proxy bro -- --help # force a bro flag name through to claude diff --git a/src/cli.js b/src/cli.js index e25f5e3..9c26824 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,5 +1,5 @@ import { readFileSync } from 'node:fs'; -import { loadConfig, ensureDefaultConfig, setKey, CONFIG_PATH } from './config.js'; +import { loadConfig, ensureDefaultConfig, setKey, configPermissionMode, CONFIG_PATH } from './config.js'; import { loadModels, mergeProviders, updateModels, REMOTE_URL } from './models.js'; import { select, promptHidden } from './ui.js'; import { launch } from './launch.js'; @@ -28,7 +28,8 @@ Usage: bro -l, --list List every provider and model bro update Refresh the model list from GitHub and cache it bro --dry-run Show what would run; launch nothing - bro --safe Don't pass --dangerously-skip-permissions + bro --safe Start Claude in manual mode (prompt for everything) + instead of the default auto mode bro -h, --help Show this help bro -v, --version Show version bro --resume Pick provider/model, then pass args to claude @@ -155,20 +156,23 @@ export async function main(argv) { // Account pool: its own setup → start proxy → launch claude flow. if (provider.mode === 'pool') { - const skipPool = !args.safe && config.dangerouslySkipPermissions !== false; + const poolMode = args.safe ? 'manual' : configPermissionMode(config); if (!args.dryRun) rememberSelection(provider.id, ''); const result = await runPool({ extraArgs: args._, - skipPermissions: skipPool, + permissionMode: poolMode, dryRun: args.dryRun }); if (args.dryRun) { console.log(JSON.stringify(result, null, 2)); return 0; } return typeof result === 'number' ? result : 0; } - // 2) model (+ an easy skip-permissions toggle — Tab to flip) + // 2) model. Claude starts in auto mode by default; --safe forces manual. + // The "Skip permissions" toggle (Tab to flip) opts into the dangerous + // --dangerously-skip-permissions bypass for this launch. let model = args.model; - let skip = !args.safe && config.dangerouslySkipPermissions !== false; + // 'auto' | 'manual' | 'bypass' + let mode = args.safe ? 'manual' : configPermissionMode(config); const models = provider.models || []; if (model == null) { if (!models.length) { @@ -179,11 +183,15 @@ export async function main(argv) { message: `Choose a model for ${provider.name || provider.id}:`, startIndex: lastM != null ? Math.max(0, models.findIndex((m) => (m.id ?? '') === lastM)) : 0, choices: models.map((m) => ({ label: modelLabel(m), value: m.id ?? '' })), - toggle: { label: 'Skip permissions', value: skip } + toggle: { label: 'Skip permissions', value: mode === 'bypass' } }).catch(() => null); if (choice == null) { console.log('Cancelled.'); return 0; } model = choice.value; - if (choice.toggleOn !== undefined) skip = choice.toggleOn; + // Turning the toggle on means bypass; turning it off drops back to the + // non-bypass posture (manual under --safe, otherwise auto). + if (choice.toggleOn !== undefined) { + mode = choice.toggleOn ? 'bypass' : args.safe ? 'manual' : 'auto'; + } } } @@ -210,7 +218,7 @@ export async function main(argv) { model, apiKey, extraArgs: args._, - skipPermissions: skip, + permissionMode: mode, dryRun: args.dryRun }); diff --git a/src/config.js b/src/config.js index 75eed04..00732a9 100644 --- a/src/config.js +++ b/src/config.js @@ -11,7 +11,10 @@ export const CONFIG_PATH = path.join(BRO_DIR, 'config.json'); const DEFAULT_CONFIG = { '#': 'bro config. Anything whose key/id/name starts with # is ignored.', '#docs': 'https://justgains.com', - dangerouslySkipPermissions: true, + // How Claude Code starts: 'auto' (intelligent auto-mode, the default), + // 'manual' (prompt for everything), or 'bypass' (--dangerously-skip-permissions). + permissionMode: 'auto', + '#dangerouslySkipPermissions': 'deprecated — use permissionMode instead (true → bypass, false → manual)', keys: { '#sakana': 'fish_xxx (remove the # and rename the key to "sakana" to use it)', '#openrouter': 'sk-or-xxx', @@ -53,6 +56,21 @@ export function ensureDefaultConfig() { return true; } +// Resolve the Claude Code permission posture from config: +// 'auto' — Claude's intelligent auto-mode (--permission-mode auto) [default] +// 'manual' — prompt for everything (Claude's normal mode) +// 'bypass' — skip every permission check (--dangerously-skip-permissions) +// `permissionMode` is the source of truth; the legacy `dangerouslySkipPermissions` +// boolean is honored only when `permissionMode` is unset, so existing configs keep +// working after upgrading. +export function configPermissionMode(config = {}) { + const m = config.permissionMode; + if (m === 'auto' || m === 'manual' || m === 'bypass') return m; + if (config.dangerouslySkipPermissions === true) return 'bypass'; + if (config.dangerouslySkipPermissions === false) return 'manual'; + return 'auto'; +} + // Persist a key without disturbing the user's '#' notes/examples. export function setKey(providerId, key) { const raw = loadRawConfig() ?? structuredClone(DEFAULT_CONFIG); diff --git a/src/launch.js b/src/launch.js index 03a220e..aa3b481 100644 --- a/src/launch.js +++ b/src/launch.js @@ -5,6 +5,14 @@ import { which, globalBinDirs, runInherit, ensureProxy } from './proc.js'; const CCR_CONFIG = path.join(os.homedir(), '.claude-code-router', 'config.json'); +// Map a permission posture ('auto' | 'manual' | 'bypass') to the claude CLI flags +// that put it in that mode at startup. Shared by the direct and pool launchers. +export function permissionArgs(mode) { + if (mode === 'bypass') return ['--dangerously-skip-permissions']; + if (mode === 'manual') return []; + return ['--permission-mode', 'auto']; +} + // Upsert this provider into the proxy's config and point its default route at the // chosen model. Existing (hand-edited) providers in the file are preserved. function writeCcrConfig(provider, model, apiKey) { @@ -42,9 +50,8 @@ function writeCcrConfig(provider, model, apiKey) { // anthropic -> point claude at an Anthropic-compatible base URL // openai -> route claude through the proxy (ccr) // With { dryRun: true } nothing is spawned or written; returns a description. -export async function launch({ provider, model, apiKey, extraArgs = [], skipPermissions = true, dryRun = false }) { - const claudeArgs = []; - if (skipPermissions) claudeArgs.push('--dangerously-skip-permissions'); +export async function launch({ provider, model, apiKey, extraArgs = [], permissionMode = 'auto', dryRun = false }) { + const claudeArgs = [...permissionArgs(permissionMode)]; if (model) claudeArgs.push('--model', provider.mode === 'openai' ? `${provider.id},${model}` : model); claudeArgs.push(...extraArgs); diff --git a/src/pool.js b/src/pool.js index a023afd..0883ea9 100644 --- a/src/pool.js +++ b/src/pool.js @@ -16,6 +16,7 @@ import path from 'node:path'; import { spawn } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { which, globalBinDirs, runInherit } from './proc.js'; +import { permissionArgs } from './launch.js'; import { select, prompt, holdOrContinue } from './ui.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -215,7 +216,7 @@ function printStatus(status, baseUrl) { // --- entry point ----------------------------------------------------------- -export async function runPool({ extraArgs = [], skipPermissions = true, dryRun = false } = {}) { +export async function runPool({ extraArgs = [], permissionMode = 'auto', dryRun = false } = {}) { const port = Number.parseInt(process.env.PORT || '', 10) || DEFAULT_PORT; const baseUrl = `http://127.0.0.1:${port}`; @@ -229,7 +230,7 @@ export async function runPool({ extraArgs = [], skipPermissions = true, dryRun = accounts: listAccounts(), claude: { cmd: which('claude') || 'claude', - args: [...(skipPermissions ? ['--dangerously-skip-permissions'] : []), ...extraArgs], + args: [...permissionArgs(permissionMode), ...extraArgs], env: { ANTHROPIC_BASE_URL: baseUrl } } }; @@ -298,9 +299,7 @@ export async function runPool({ extraArgs = [], skipPermissions = true, dryRun = env.ANTHROPIC_AUTH_TOKEN = process.env.PROXY_API_KEY || 'claude-max-pool'; env.NODE_NO_WARNINGS = '1'; - const claudeArgs = []; - if (skipPermissions) claudeArgs.push('--dangerously-skip-permissions'); - claudeArgs.push(...extraArgs); + const claudeArgs = [...permissionArgs(permissionMode), ...extraArgs]; console.log('Launching Claude Code through the account pool…\n'); try {