diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 6f4508cb0b0..c8856b28bfe 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -229,6 +229,7 @@ export const RunCommand = effectCmd({ default: false, }) .option("dangerously-skip-permissions", { + alias: ["yolo"], type: "boolean", describe: "auto-approve permissions that are not explicitly denied (dangerous!)", default: false, diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index 68941e976ac..cfb5c7b267e 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -103,6 +103,12 @@ export const TuiThreadCommand = cmd({ .option("agent", { type: "string", describe: "agent to use", + }) + .option("dangerously-skip-permissions", { + alias: ["yolo"], + type: "boolean", + describe: "auto-approve permissions that are not explicitly denied (dangerous!)", + default: false, }), handler: async (args) => { const unguard = win32InstallCtrlCGuard() diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 58dc50d0278..39a596fdf28 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -15,6 +15,7 @@ const enabledByExperimental = (name: string) => export class Service extends ConfigService.Service()("@opencode/RuntimeFlags", { autoShare: bool("OPENCODE_AUTO_SHARE"), + dangerouslySkipPermissions: bool("OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS"), pure: bool("OPENCODE_PURE"), disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"), disableEmbeddedWebUi: bool("OPENCODE_DISABLE_EMBEDDED_WEB_UI"), diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 13540a73a36..0cf53aeb9bf 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -66,6 +66,7 @@ const cli = yargs(args) .middleware(async (opts) => { if (opts.printLogs) process.env.OPENCODE_PRINT_LOGS = "1" if (opts.logLevel) process.env.OPENCODE_LOG_LEVEL = opts.logLevel + if (opts.dangerouslySkipPermissions) process.env.OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS = "1" if (opts.pure) { process.env.OPENCODE_PURE = "1" } diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index cd1f935adaf..6cec0abebd6 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -7,6 +7,7 @@ import os from "os" import { PermissionV1 } from "@opencode-ai/core/v1/permission" import { EventV2Bridge } from "@/event-v2-bridge" import { EventV2 } from "@opencode-ai/core/event" +import { RuntimeFlags } from "@/effect/runtime-flags" export const Event = { Asked: EventV2.define({ type: "permission.asked", schema: PermissionV1.Request.fields }), @@ -54,6 +55,7 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const events = yield* EventV2Bridge.Service + const flags = yield* RuntimeFlags.Service const state = yield* InstanceState.make( Effect.fn("Permission.state")(function* (ctx) { void ctx @@ -89,6 +91,7 @@ export const layer = Layer.effect( }) } if (rule.action === "allow") continue + if (flags.dangerouslySkipPermissions) continue needsAsk = true } @@ -223,8 +226,11 @@ export function disabled(tools: string[], ruleset: PermissionV1.Ruleset): Set { const topLevel = yield* opencode.spawn(["--help"], { env: SNAPSHOT_ENV }) expect(topLevel.exitCode).toBe(0) expect(topLevel.stderr.endsWith(EOL)).toBe(true) + expect(topLevel.stderr).toContain("--dangerously-skip-permissions") + expect(topLevel.stderr).toContain("--yolo") const argvs: Array = [...TOP_LEVEL.map((c) => [c] as const), ...SUBCOMMANDS] diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index 2e1226b38ba..f029e6c0b67 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -14,6 +14,7 @@ describe("RuntimeFlags", () => { const flags = yield* readFlags.pipe(Effect.provide(fromConfig({}))) expect(flags.autoShare).toBe(false) + expect(flags.dangerouslySkipPermissions).toBe(false) }), ) @@ -23,6 +24,7 @@ describe("RuntimeFlags", () => { Effect.provide( fromConfig({ OPENCODE_PURE: "true", + OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS: "true", OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", OPENCODE_AUTO_SHARE: "true", OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true", @@ -39,6 +41,7 @@ describe("RuntimeFlags", () => { ) expect(flags.pure).toBe(true) + expect(flags.dangerouslySkipPermissions).toBe(true) expect(flags.autoShare).toBe(true) expect(flags.disableDefaultPlugins).toBe(true) expect(flags.disableEmbeddedWebUi).toBe(true) diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index e784350055a..6a405a09dd4 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -11,16 +11,21 @@ import { InstanceStore } from "../../src/project/instance-store" import { TestInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { MessageID, SessionID } from "../../src/session/schema" +import { RuntimeFlags } from "../../src/effect/runtime-flags" const events = EventV2Bridge.defaultLayer const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void })) -const env = Layer.mergeAll( - Permission.layer.pipe(Layer.provide(Database.defaultLayer), Layer.provide(events)), - events, - CrossSpawnSpawner.defaultLayer, - InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)), -) -const it = testEffect(env) +const makeEnv = (flags: Partial = {}) => { + const runtimeFlags = RuntimeFlags.layer(flags) + return Layer.mergeAll( + Permission.layer.pipe(Layer.provide(Database.defaultLayer), Layer.provide(events), Layer.provide(runtimeFlags)), + events, + CrossSpawnSpawner.defaultLayer, + InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap)), + ) +} +const it = testEffect(makeEnv()) +const dangerousIt = testEffect(makeEnv({ dangerouslySkipPermissions: true })) const rejectAll = (message?: string) => Effect.gen(function* () { @@ -613,6 +618,39 @@ it.instance( { git: true }, ) +dangerousIt.instance( + "ask - resolves ask rules when dangerous permission skipping is enabled", + () => + ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["ls"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "ask" }], + }), + { git: true }, +) + +dangerousIt.instance( + "ask - preserves deny rules when dangerous permission skipping is enabled", + () => + Effect.gen(function* () { + const err = yield* fail( + ask({ + sessionID: SessionID.make("session_test"), + permission: "bash", + patterns: ["rm -rf /"], + metadata: {}, + always: [], + ruleset: [{ permission: "bash", pattern: "*", action: "deny" }], + }), + ) + expect(err).toBeInstanceOf(PermissionV1.DeniedError) + }), + { git: true }, +) + it.instance( "ask - adds request to pending list", () => diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 0f9e5e3b1f4..3d7a98aa0e6 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -29,19 +29,20 @@ opencode [project] #### Flags -| Flag | Short | Description | -| ------------------------------------------- | ----- | ----------------------------------------------------------------------- | -| {"--continue"} | `-c` | Continue the last session | -| {"--session"} | `-s` | Session ID to continue | -| {"--fork"} | | Fork the session when continuing (use with `--continue` or `--session`) | -| {"--prompt"} | | Prompt to use | -| {"--model"} | `-m` | Model to use in the form of provider/model | -| {"--agent"} | | Agent to use | -| {"--port"} | | Port to listen on | -| {"--hostname"} | | Hostname to listen on | -| {"--mdns"} | | Enable mDNS discovery | -| {"--mdns-domain"} | | Custom mDNS domain name | -| {"--cors"} | | Additional browser origin(s) to allow CORS | +| Flag | Short | Description | +| ------------------------------------------------------------ | ----- | ----------------------------------------------------------------------- | +| {"--continue"} | `-c` | Continue the last session | +| {"--session"} | `-s` | Session ID to continue | +| {"--fork"} | | Fork the session when continuing (use with `--continue` or `--session`) | +| {"--prompt"} | | Prompt to use | +| {"--model"} | `-m` | Model to use in the form of provider/model | +| {"--agent"} | | Agent to use | +| {"--port"} | | Port to listen on | +| {"--hostname"} | | Hostname to listen on | +| {"--mdns"} | | Enable mDNS discovery | +| {"--mdns-domain"} | | Custom mDNS domain name | +| {"--cors"} | | Additional browser origin(s) to allow CORS | +| {"--dangerously-skip-permissions"} | | Auto-approve permissions not explicitly denied. Alias: `--yolo` | --- @@ -380,7 +381,7 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript" | {"--port"} | | Port for the local server (defaults to random port) | | {"--variant"} | | Model variant (provider-specific reasoning effort) | | {"--thinking"} | | Show thinking blocks | -| {"--dangerously-skip-permissions"} | | Auto-approve permissions that are not explicitly denied (dangerous!) | +| {"--dangerously-skip-permissions"} | | Auto-approve permissions that are not explicitly denied. Alias: `--yolo` | --- @@ -676,33 +677,34 @@ The opencode CLI takes the following global flags. OpenCode can be configured using environment variables. -| Variable | Type | Description | -| ------------------------------------- | ------- | ------------------------------------------------- | -| `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions | -| `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows | -| `OPENCODE_CONFIG` | string | Path to config file | -| `OPENCODE_TUI_CONFIG` | string | Path to TUI config file | -| `OPENCODE_CONFIG_DIR` | string | Path to config directory | -| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content | -| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks | -| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data | -| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates | -| `OPENCODE_PERMISSION` | string | Inlined json permissions config | -| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Disable default plugins | -| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Disable automatic LSP server downloads | -| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Enable experimental models | -| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Disable automatic context compaction | -| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Disable reading from `.claude` (prompt + skills) | -| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Disable reading `~/.claude/CLAUDE.md` | -| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Disable loading `.claude/skills` | -| `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Disable fetching models from remote sources | -| `OPENCODE_DISABLE_MOUSE` | boolean | Disable mouse capture in the TUI | -| `OPENCODE_FAKE_VCS` | string | Fake VCS provider for testing purposes | -| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) | -| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools | -| `OPENCODE_SERVER_PASSWORD` | string | Enable basic auth for `serve`/`web` | -| `OPENCODE_SERVER_USERNAME` | string | Override basic auth username (default `opencode`) | -| `OPENCODE_MODELS_URL` | string | Custom URL for fetching models configuration | +| Variable | Type | Description | +| --------------------------------------- | ------- | ------------------------------------------------- | +| `OPENCODE_AUTO_SHARE` | boolean | Automatically share sessions | +| `OPENCODE_GIT_BASH_PATH` | string | Path to Git Bash executable on Windows | +| `OPENCODE_CONFIG` | string | Path to config file | +| `OPENCODE_TUI_CONFIG` | string | Path to TUI config file | +| `OPENCODE_CONFIG_DIR` | string | Path to config directory | +| `OPENCODE_CONFIG_CONTENT` | string | Inline json config content | +| `OPENCODE_DISABLE_AUTOUPDATE` | boolean | Disable automatic update checks | +| `OPENCODE_DISABLE_PRUNE` | boolean | Disable pruning of old data | +| `OPENCODE_DISABLE_TERMINAL_TITLE` | boolean | Disable automatic terminal title updates | +| `OPENCODE_PERMISSION` | string | Inlined json permissions config | +| `OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS` | boolean | Auto-approve permissions not explicitly denied | +| `OPENCODE_DISABLE_DEFAULT_PLUGINS` | boolean | Disable default plugins | +| `OPENCODE_DISABLE_LSP_DOWNLOAD` | boolean | Disable automatic LSP server downloads | +| `OPENCODE_ENABLE_EXPERIMENTAL_MODELS` | boolean | Enable experimental models | +| `OPENCODE_DISABLE_AUTOCOMPACT` | boolean | Disable automatic context compaction | +| `OPENCODE_DISABLE_CLAUDE_CODE` | boolean | Disable reading from `.claude` (prompt + skills) | +| `OPENCODE_DISABLE_CLAUDE_CODE_PROMPT` | boolean | Disable reading `~/.claude/CLAUDE.md` | +| `OPENCODE_DISABLE_CLAUDE_CODE_SKILLS` | boolean | Disable loading `.claude/skills` | +| `OPENCODE_DISABLE_MODELS_FETCH` | boolean | Disable fetching models from remote sources | +| `OPENCODE_DISABLE_MOUSE` | boolean | Disable mouse capture in the TUI | +| `OPENCODE_FAKE_VCS` | string | Fake VCS provider for testing purposes | +| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) | +| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools | +| `OPENCODE_SERVER_PASSWORD` | string | Enable basic auth for `serve`/`web` | +| `OPENCODE_SERVER_USERNAME` | string | Override basic auth username (default `opencode`) | +| `OPENCODE_MODELS_URL` | string | Custom URL for fetching models configuration | ---