Skip to content
Draft
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
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 <session-id> # pick provider/model, then resume Claude there
bro -p pool --resume <id> # resume through the Multiple Claude Account Proxy
bro -- --help # force a bro flag name through to claude
Expand Down
26 changes: 17 additions & 9 deletions src/cli.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 <id> Pick provider/model, then pass args to claude
Expand Down Expand Up @@ -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) {
Expand All @@ -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';
}
}
}

Expand All @@ -210,7 +218,7 @@ export async function main(argv) {
model,
apiKey,
extraArgs: args._,
skipPermissions: skip,
permissionMode: mode,
dryRun: args.dryRun
});

Expand Down
20 changes: 19 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 10 additions & 3 deletions src/launch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand Down
9 changes: 4 additions & 5 deletions src/pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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}`;

Expand All @@ -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 }
}
};
Expand Down Expand Up @@ -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 {
Expand Down