mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 02:34:15 -05:00
feat(cli): add --yolo alias for --dangerously-skip-permissions (#2097)
Register the alias on the main command and the ssh stub. Recognize it in the cc:// and ssh raw-argv scans, and in both skills pre-parse boolean sets (leading and trailing), so and route correctly. Update the web flags docs. Includes source-scan + help-text tests proving the alias is wired through. The SSH/argv refactor remains on the existing feat/yolo-flag branch for a separate follow-up PR.
This commit is contained in:
@@ -14,6 +14,7 @@ const TRAILING_GLOBAL_BOOLEAN_FLAGS = new Set([
|
||||
'--bare',
|
||||
'--debug',
|
||||
'--debug-to-stderr',
|
||||
'--yolo',
|
||||
'--dangerously-skip-permissions',
|
||||
'--allow-dangerously-skip-permissions',
|
||||
'--disable-slash-commands',
|
||||
|
||||
@@ -565,4 +565,99 @@ describe('Node 24 premature exit regression (issue #1678)', () => {
|
||||
expect(src).toMatch(/await main\(\)/)
|
||||
expect(src).not.toMatch(/^\s*void main\(\)/m)
|
||||
})
|
||||
|
||||
describe('--yolo alias', () => {
|
||||
it('is registered on the main command next to the canonical flag', async () => {
|
||||
const src = await Bun.file(`${import.meta.dir}/../main.tsx`).text()
|
||||
expect(src).toContain(
|
||||
".option('--yolo, --dangerously-skip-permissions', 'Bypass all permission checks",
|
||||
)
|
||||
})
|
||||
|
||||
it('is registered on the ssh stub command', async () => {
|
||||
const src = await Bun.file(`${import.meta.dir}/../main.tsx`).text()
|
||||
const sshCmd = src.indexOf("program.command('ssh <host> [dir]')")
|
||||
expect(sshCmd).toBeGreaterThanOrEqual(0)
|
||||
const sshAction = src.indexOf('.action(async () => {', sshCmd)
|
||||
const sshBlock = src.slice(sshCmd, sshAction)
|
||||
expect(sshBlock).toContain(
|
||||
"--yolo, --dangerously-skip-permissions",
|
||||
)
|
||||
})
|
||||
|
||||
it('is recognized by the cc:// and ssh raw-argv scans', async () => {
|
||||
const src = await Bun.file(`${import.meta.dir}/../main.tsx`).text()
|
||||
// cc:// sets remote state via includes(); the rewrites and ssh path strip
|
||||
// both spellings from the forwarded argv.
|
||||
expect(src).toContain(
|
||||
"rawCliArgs.includes('--dangerously-skip-permissions') || rawCliArgs.includes('--yolo')",
|
||||
)
|
||||
expect(src).toContain("arg !== '--dangerously-skip-permissions' && arg !== '--yolo'")
|
||||
expect(src).toContain(
|
||||
"if (arg === '--dangerously-skip-permissions' || arg === '--yolo')",
|
||||
)
|
||||
})
|
||||
|
||||
it('strips both bypass spellings from cc:// and ssh forwarded argv', async () => {
|
||||
const src = await Bun.file(`${import.meta.dir}/../main.tsx`).text()
|
||||
// Passing both flags at once must not leave one behind as an unknown
|
||||
// option on the headless `open` subcommand or in the ssh forwarded line.
|
||||
const ccBlockStart = src.indexOf('Check for cc:// or cc+unix:// URL in argv')
|
||||
const ccBlockEnd = src.indexOf('// Handle deep link URIs early', ccBlockStart)
|
||||
const ccBlock = src.slice(ccBlockStart, ccBlockEnd)
|
||||
const ccOccurrences =
|
||||
ccBlock.split("'--dangerously-skip-permissions'").length - 1 +
|
||||
ccBlock.split("'--yolo'").length - 1
|
||||
expect(ccOccurrences).toBeGreaterThanOrEqual(4)
|
||||
|
||||
const sshBlockStart = src.indexOf("if (rawCliArgs[0] === 'ssh')")
|
||||
const sshBlockEnd = src.indexOf('// else: `claude ssh` with no host', sshBlockStart)
|
||||
const sshBlock = src.slice(sshBlockStart, sshBlockEnd)
|
||||
expect(sshBlock).toContain(
|
||||
"if (arg === '--dangerously-skip-permissions' || arg === '--yolo')",
|
||||
)
|
||||
})
|
||||
|
||||
it('is recognized by the skills leading scan so --yolo skills list routes', async () => {
|
||||
const src = await Bun.file(`${import.meta.dir}/cli.tsx`).text()
|
||||
const setStart = src.indexOf('SKILLS_LEADING_BOOLEAN_FLAGS = new Set([')
|
||||
expect(setStart).toBeGreaterThanOrEqual(0)
|
||||
const setEnd = src.indexOf(']', setStart)
|
||||
const setBody = src.slice(setStart, setEnd)
|
||||
expect(setBody).toContain("'--yolo'")
|
||||
})
|
||||
|
||||
it('is recognized by the skills trailing scan so skills list --yolo routes', async () => {
|
||||
const src = await Bun.file(
|
||||
`${import.meta.dir}/../cli/handlers/skillsCli.ts`,
|
||||
).text()
|
||||
const setStart = src.indexOf('TRAILING_GLOBAL_BOOLEAN_FLAGS = new Set([')
|
||||
expect(setStart).toBeGreaterThanOrEqual(0)
|
||||
const setEnd = src.indexOf(']', setStart)
|
||||
const setBody = src.slice(setStart, setEnd)
|
||||
expect(setBody).toContain("'--yolo'")
|
||||
})
|
||||
|
||||
it('appears in the built CLI help', async () => {
|
||||
const fs = await import('node:fs')
|
||||
const path = await import('node:path')
|
||||
const cliPath = path.resolve(import.meta.dir, '../../dist/cli.mjs')
|
||||
expect(fs.existsSync(cliPath)).toBe(true)
|
||||
|
||||
const originalGuard = process.env.OPENCLAUDE_DISABLE_CLI_ENTRYPOINT_AUTO_RUN
|
||||
delete process.env.OPENCLAUDE_DISABLE_CLI_ENTRYPOINT_AUTO_RUN
|
||||
try {
|
||||
const proc = Bun.spawn(['node', cliPath, '--help'], { stdout: 'pipe' })
|
||||
const text = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
expect(text).toContain('--yolo, --dangerously-skip-permissions')
|
||||
} finally {
|
||||
if (originalGuard === undefined) {
|
||||
delete process.env.OPENCLAUDE_DISABLE_CLI_ENTRYPOINT_AUTO_RUN
|
||||
} else {
|
||||
process.env.OPENCLAUDE_DISABLE_CLI_ENTRYPOINT_AUTO_RUN = originalGuard
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -42,6 +42,7 @@ const SKILLS_LEADING_BOOLEAN_FLAGS = new Set([
|
||||
'--bare',
|
||||
'--debug',
|
||||
'--debug-to-stderr',
|
||||
'--yolo',
|
||||
'--dangerously-skip-permissions',
|
||||
'--allow-dangerously-skip-permissions',
|
||||
'--disable-slash-commands',
|
||||
|
||||
+21
-19
@@ -589,24 +589,22 @@ export async function main() {
|
||||
parseConnectUrl
|
||||
} = await import('./server/parseConnectUrl.js');
|
||||
const parsed = parseConnectUrl(ccUrl);
|
||||
_pendingConnect.dangerouslySkipPermissions = rawCliArgs.includes('--dangerously-skip-permissions');
|
||||
_pendingConnect.dangerouslySkipPermissions = rawCliArgs.includes('--dangerously-skip-permissions') || rawCliArgs.includes('--yolo');
|
||||
if (rawCliArgs.includes('-p') || rawCliArgs.includes('--print')) {
|
||||
// Headless: rewrite to internal `open` subcommand
|
||||
const stripped = rawCliArgs.filter((_, i) => i !== ccIdx);
|
||||
const dspIdx = stripped.indexOf('--dangerously-skip-permissions');
|
||||
if (dspIdx !== -1) {
|
||||
stripped.splice(dspIdx, 1);
|
||||
}
|
||||
// Headless: rewrite to internal `open` subcommand. Strip both the
|
||||
// canonical flag and its alias — the `open` stub does not register
|
||||
// either, and passing both would leave one behind as an unknown option.
|
||||
const stripped = rawCliArgs
|
||||
.filter((_, i) => i !== ccIdx)
|
||||
.filter(arg => arg !== '--dangerously-skip-permissions' && arg !== '--yolo');
|
||||
process.argv = [process.argv[0]!, process.argv[1]!, 'open', ccUrl, ...stripped];
|
||||
} else {
|
||||
// Interactive: strip cc:// URL and flags, run main command
|
||||
// Interactive: strip cc:// URL and both bypass spellings, run main command
|
||||
_pendingConnect.url = parsed.serverUrl;
|
||||
_pendingConnect.authToken = parsed.authToken;
|
||||
const stripped = rawCliArgs.filter((_, i) => i !== ccIdx);
|
||||
const dspIdx = stripped.indexOf('--dangerously-skip-permissions');
|
||||
if (dspIdx !== -1) {
|
||||
stripped.splice(dspIdx, 1);
|
||||
}
|
||||
const stripped = rawCliArgs
|
||||
.filter((_, i) => i !== ccIdx)
|
||||
.filter(arg => arg !== '--dangerously-skip-permissions' && arg !== '--yolo');
|
||||
process.argv = [process.argv[0]!, process.argv[1]!, ...stripped];
|
||||
}
|
||||
}
|
||||
@@ -688,10 +686,14 @@ export async function main() {
|
||||
_pendingSSH.local = true;
|
||||
rawCliArgs.splice(localIdx, 1);
|
||||
}
|
||||
const dspIdx = rawCliArgs.indexOf('--dangerously-skip-permissions');
|
||||
if (dspIdx !== -1) {
|
||||
_pendingSSH.dangerouslySkipPermissions = true;
|
||||
rawCliArgs.splice(dspIdx, 1);
|
||||
// Remove both bypass spellings from the forwarded argv; the remote state
|
||||
// is carried by _pendingSSH.dangerouslySkipPermissions, not by a flag.
|
||||
for (let i = rawCliArgs.length - 1; i >= 0; i -= 1) {
|
||||
const arg = rawCliArgs[i];
|
||||
if (arg === '--dangerously-skip-permissions' || arg === '--yolo') {
|
||||
_pendingSSH.dangerouslySkipPermissions = true;
|
||||
rawCliArgs.splice(i, 1);
|
||||
}
|
||||
}
|
||||
const pmIdx = rawCliArgs.indexOf('--permission-mode');
|
||||
if (pmIdx !== -1 && rawCliArgs[pmIdx + 1] && !rawCliArgs[pmIdx + 1]!.startsWith('-')) {
|
||||
@@ -946,7 +948,7 @@ async function run(): Promise<CommanderCommand> {
|
||||
} catch (error) {
|
||||
throw new InvalidArgumentError(errorMessage(error));
|
||||
}
|
||||
})).option('--bare', 'Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials. Skills still resolve via /skill-name. Explicitly provide context via: --system-prompt[-file], --append-system-prompt[-file], --add-dir (CLAUDE.md dirs), --mcp-config, --settings, --agents, --plugin-dir.', () => true).addOption(new Option('--init', 'Run Setup hooks with init trigger, then continue').hideHelp()).addOption(new Option('--init-only', 'Run Setup and SessionStart:startup hooks, then exit').hideHelp()).addOption(new Option('--maintenance', 'Run Setup hooks with maintenance trigger, then continue').hideHelp()).addOption(new Option('--output-format <format>', 'Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming)').choices(['text', 'json', 'stream-json'])).addOption(new Option('--json-schema <schema>', 'JSON Schema for structured output validation. ' + 'Example: {"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}').argParser(String)).option('--include-hook-events', 'Include all hook lifecycle events in the output stream (only works with --output-format=stream-json)', () => true).option('--include-partial-messages', 'Include partial message chunks as they arrive (only works with --print and --output-format=stream-json)', () => true).addOption(new Option('--input-format <format>', 'Input format (only works with --print): "text" (default), or "stream-json" (realtime streaming input)').choices(['text', 'stream-json'])).option('--mcp-debug', '[DEPRECATED. Use --debug instead] Enable MCP debug mode (shows MCP server errors)', () => true).option('--dangerously-skip-permissions', 'Bypass all permission checks. Recommended only for sandboxes with no internet access.', () => true).option('--allow-dangerously-skip-permissions', 'Enable bypassing all permission checks as an option, without it being enabled by default. Recommended only for sandboxes with no internet access.', () => true).addOption(new Option('--thinking <mode>', 'Thinking mode: enabled (equivalent to adaptive), disabled').choices(['enabled', 'adaptive', 'disabled']).hideHelp()).addOption(new Option('--max-thinking-tokens <tokens>', '[DEPRECATED. Use --thinking instead for newer models] Maximum number of thinking tokens (only works with --print)').argParser(Number).hideHelp()).addOption(new Option('--max-turns <turns>', MAX_TURNS_CLI_DESCRIPTION).argParser(value => {
|
||||
})).option('--bare', 'Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials. Skills still resolve via /skill-name. Explicitly provide context via: --system-prompt[-file], --append-system-prompt[-file], --add-dir (CLAUDE.md dirs), --mcp-config, --settings, --agents, --plugin-dir.', () => true).addOption(new Option('--init', 'Run Setup hooks with init trigger, then continue').hideHelp()).addOption(new Option('--init-only', 'Run Setup and SessionStart:startup hooks, then exit').hideHelp()).addOption(new Option('--maintenance', 'Run Setup hooks with maintenance trigger, then continue').hideHelp()).addOption(new Option('--output-format <format>', 'Output format (only works with --print): "text" (default), "json" (single result), or "stream-json" (realtime streaming)').choices(['text', 'json', 'stream-json'])).addOption(new Option('--json-schema <schema>', 'JSON Schema for structured output validation. ' + 'Example: {"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}').argParser(String)).option('--include-hook-events', 'Include all hook lifecycle events in the output stream (only works with --output-format=stream-json)', () => true).option('--include-partial-messages', 'Include partial message chunks as they arrive (only works with --print and --output-format=stream-json)', () => true).addOption(new Option('--input-format <format>', 'Input format (only works with --print): "text" (default), or "stream-json" (realtime streaming input)').choices(['text', 'stream-json'])).option('--mcp-debug', '[DEPRECATED. Use --debug instead] Enable MCP debug mode (shows MCP server errors)', () => true).option('--yolo, --dangerously-skip-permissions', 'Bypass all permission checks. Recommended only for sandboxes with no internet access.', () => true).option('--allow-dangerously-skip-permissions', 'Enable bypassing all permission checks as an option, without it being enabled by default. Recommended only for sandboxes with no internet access.', () => true).addOption(new Option('--thinking <mode>', 'Thinking mode: enabled (equivalent to adaptive), disabled').choices(['enabled', 'adaptive', 'disabled']).hideHelp()).addOption(new Option('--max-thinking-tokens <tokens>', '[DEPRECATED. Use --thinking instead for newer models] Maximum number of thinking tokens (only works with --print)').argParser(Number).hideHelp()).addOption(new Option('--max-turns <turns>', MAX_TURNS_CLI_DESCRIPTION).argParser(value => {
|
||||
return parseMaxTurnsCommanderArgument(value);
|
||||
})).addOption(new Option('--max-budget-usd <amount>', 'Maximum dollar amount to spend on API calls (only works with --print)').argParser(value => {
|
||||
const amount = Number(value);
|
||||
@@ -3912,7 +3914,7 @@ async function run(): Promise<CommanderCommand> {
|
||||
// this action it means the argv rewrite didn't fire (e.g. user ran
|
||||
// `claude ssh` with no host) — just print usage.
|
||||
if (feature('SSH_REMOTE')) {
|
||||
program.command('ssh <host> [dir]').description('Run OpenClaude on a remote host over SSH. Deploys the binary and ' + 'tunnels API auth back through your local machine — no remote setup needed.').option('--permission-mode <mode>', 'Permission mode for the remote session').option('--dangerously-skip-permissions', 'Skip all permission prompts on the remote (dangerous)').option('--local', 'e2e test mode — spawn the child CLI locally (skip ssh/deploy). ' + 'Exercises the auth proxy and unix-socket plumbing without a remote host.').action(async () => {
|
||||
program.command('ssh <host> [dir]').description('Run OpenClaude on a remote host over SSH. Deploys the binary and ' + 'tunnels API auth back through your local machine — no remote setup needed.').option('--permission-mode <mode>', 'Permission mode for the remote session').option('--yolo, --dangerously-skip-permissions', 'Skip all permission prompts on the remote (dangerous)').option('--local', 'e2e test mode — spawn the child CLI locally (skip ssh/deploy). ' + 'Exercises the auth proxy and unix-socket plumbing without a remote host.').action(async () => {
|
||||
// Argv rewriting in main() should have consumed `ssh <host>` before
|
||||
// commander runs. Reaching here means host was missing or the
|
||||
// rewrite predicate didn't match.
|
||||
|
||||
@@ -83,7 +83,7 @@ export const flagGroups: FlagGroup[] = [
|
||||
{ flag: '--allowed-tools', arg: '<tools...>', description: 'Comma or space-separated list of tool rules to allow (e.g. "Bash(git:*) Edit").' },
|
||||
{ flag: '--disallowed-tools', arg: '<tools...>', description: 'Comma or space-separated list of tool rules to deny.' },
|
||||
{ flag: '--tools', arg: '<tools...>', description: 'Restrict the built-in tool set: "" disables all tools, "default" enables all, or list names like "Bash,Edit,Read".' },
|
||||
{ flag: '--dangerously-skip-permissions', description: 'Bypass all permission checks. Recommended only for sandboxes with no internet access.' },
|
||||
{ flag: '--yolo, --dangerously-skip-permissions', description: 'Bypass all permission checks. Recommended only for sandboxes with no internet access.' },
|
||||
{ flag: '--allow-dangerously-skip-permissions', description: 'Make permission bypass available as an option without enabling it by default.' },
|
||||
{ flag: '--add-dir', arg: '<dirs...>', description: 'Additional directories to allow tool access to.' },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user