Skip to content

Commit f5b5a73

Browse files
anandgupta42claude
andauthored
feat: add yolo mode to auto-approve permission prompts (#240)
* feat: add `--yolo` mode to auto-approve permission prompts - Add `ALTIMATE_CLI_YOLO` / `OPENCODE_YOLO` env var flag - Add `--yolo` as global CLI option (works with any subcommand) - Add `--yolo` on `run` command for convenience - Auto-approve (`"once"`) instead of auto-reject in headless mode - Auto-approve in TUI via env var (skip permission prompt rendering) - Global `--yolo` propagates to env var in middleware so all paths pick it up - Explicit `deny` rules still enforced (safety by construction) - Update permissions docs with Yolo Mode section Closes #239 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: yolo mode — dynamic getter, ACP support, adversarial tests - CRITICAL FIX: `Flag.ALTIMATE_CLI_YOLO` was a static `const` evaluated at module load time. `--yolo` CLI flag sets env var in middleware (after load), so the flag was always `false` via CLI. Now uses `Object.defineProperty` dynamic getter like other runtime-set flags. - Add yolo mode handling to ACP agent (`acp/agent.ts`) — was missing, causing ACP clients to hang on permission prompts even with `--yolo` - Replace unsafe `(opts as any).yolo` with `"yolo" in opts && opts.yolo` - Add 26 adversarial tests covering: - Dynamic getter timing (the critical bug) - Env var parsing edge cases (empty, arbitrary strings, case) - Deny rules cannot be bypassed by yolo (safety by construction) - Case sensitivity of deny patterns - Multiple ruleset merge precedence - Error type distinction (`DeniedError` vs `RejectedError`) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: yolo mode — dynamic getter, ACP support, adversarial tests - CRITICAL FIX: `Flag.ALTIMATE_CLI_YOLO` was a static `const` evaluated at module load time. `--yolo` CLI flag sets env var in middleware (after load), so the flag was always `false` via CLI. Now uses `Object.defineProperty` dynamic getter like other runtime-set flags. - Add yolo mode handling to ACP agent (`acp/agent.ts`) — was missing, causing ACP clients to hang on permission prompts even with `--yolo` - Replace unsafe `(opts as any).yolo` with `"yolo" in opts && opts.yolo` - Add 26 adversarial tests covering: - Dynamic getter timing (the critical bug) - Env var parsing edge cases (empty, arbitrary strings, case) - Deny rules cannot be bypassed by yolo (safety by construction) - Case sensitivity of deny patterns - Multiple ruleset merge precedence - Error type distinction (`DeniedError` vs `RejectedError`) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * feat: E2E tests, TUI yolo indicator, minor review fixes 1. **E2E tests** (6 new integration tests): - Deny rules throw `DeniedError` even with yolo env var set - Allow rules pass without asking (no yolo needed) - Ask rules wait for reply, resolved by yolo-style `"once"` - Multiple simultaneous permissions all resolved individually - Mixed deny + ask: deny throws, ask waits for reply - Config-driven permissions: yolo auto-approves `ask`, can't touch `deny` 2. **TUI yolo indicator** — `△ YOLO` warning in footer status bar when yolo mode is active, matching existing permission warning style 3. **Minor review fixes:** - Remove duplicate `--yolo` option from `run.ts` (inherited from global) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * docs: update yolo mode documentation - Remove "subcommand flag" section (duplicate --yolo removed from run.ts) - Explain WHY deny rules are still enforced (thrown before events) - Document env var precedence (ALTIMATE_CLI_YOLO overrides OPENCODE_YOLO) - Mention TUI footer indicator Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
1 parent a3d3176 commit f5b5a73

File tree

8 files changed

+610
-9
lines changed

8 files changed

+610
-9
lines changed

docs/docs/configure/permissions.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,30 @@ export ALTIMATE_CLI_PERMISSION='{"bash":"deny","write":"deny"}'
107107
altimate
108108
```
109109

110+
## Yolo Mode
111+
112+
Auto-approve all permission prompts without asking. Useful for CI/CD pipelines, benchmarks, scripted workflows, and trusted environments.
113+
114+
**CLI flag (works with any subcommand):**
115+
116+
```bash
117+
altimate-code --yolo run "build all dbt models"
118+
altimate-code --yolo # launches TUI in yolo mode
119+
```
120+
121+
**Environment variable:**
122+
123+
```bash
124+
export ALTIMATE_CLI_YOLO=true
125+
altimate-code run "analyze my queries"
126+
```
127+
128+
The fallback `OPENCODE_YOLO` env var is also supported. When both are set, `ALTIMATE_CLI_YOLO` takes precedence — setting it to `false` disables yolo even if `OPENCODE_YOLO=true`.
129+
130+
**Safety:** Explicit `deny` rules in your config are still enforced. Deny rules throw an error *before* any permission prompt is created, so yolo mode never sees them. If you've denied `rm *` or `DROP *`, those remain blocked even with `--yolo`.
131+
132+
When yolo mode is active in the TUI, a `△ YOLO` indicator appears in the footer status bar.
133+
110134
## Recommended Configurations
111135

112136
### Data Engineering (Default — Balanced)

packages/opencode/src/acp/agent.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import { z } from "zod"
4545
import { LoadAPIKeyError } from "ai"
4646
import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2"
4747
import { applyPatch } from "diff"
48+
// altimate_change start - yolo mode
49+
import { Flag } from "@/flag/flag"
50+
// altimate_change end
4851

4952
type ModeOption = { id: string; name: string; description?: string }
5053
type ModelOption = { modelId: string; name: string }
@@ -188,6 +191,17 @@ export namespace ACP {
188191
const session = this.sessionManager.tryGet(permission.sessionID)
189192
if (!session) return
190193

194+
// altimate_change start - yolo mode: auto-approve without asking ACP client
195+
if (Flag.ALTIMATE_CLI_YOLO) {
196+
await this.sdk.permission.reply({
197+
requestID: permission.id,
198+
reply: "once",
199+
directory: session.cwd,
200+
})
201+
return
202+
}
203+
// altimate_change end
204+
191205
const prev = this.permissionQueues.get(permission.sessionID) ?? Promise.resolve()
192206
const next = prev
193207
.then(async () => {

packages/opencode/src/cli/cmd/run.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -664,15 +664,30 @@ You are speaking to a non-technical business executive. Follow these rules stric
664664
if (event.type === "permission.asked") {
665665
const permission = event.properties
666666
if (permission.sessionID !== sessionID) continue
667-
UI.println(
668-
UI.Style.TEXT_WARNING_BOLD + "!",
669-
UI.Style.TEXT_NORMAL +
670-
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
671-
)
672-
await sdk.permission.reply({
673-
requestID: permission.id,
674-
reply: "reject",
675-
})
667+
// altimate_change start - yolo mode: auto-approve instead of auto-reject
668+
const yolo = args.yolo || Flag.ALTIMATE_CLI_YOLO
669+
if (yolo) {
670+
UI.println(
671+
UI.Style.TEXT_WARNING_BOLD + "!",
672+
UI.Style.TEXT_NORMAL +
673+
`yolo mode: auto-approved ${permission.permission} (${permission.patterns.join(", ")})`,
674+
)
675+
await sdk.permission.reply({
676+
requestID: permission.id,
677+
reply: "once",
678+
})
679+
} else {
680+
UI.println(
681+
UI.Style.TEXT_WARNING_BOLD + "!",
682+
UI.Style.TEXT_NORMAL +
683+
`permission requested: ${permission.permission} (${permission.patterns.join(", ")}); auto-rejecting`,
684+
)
685+
await sdk.permission.reply({
686+
requestID: permission.id,
687+
reply: "reject",
688+
})
689+
}
690+
// altimate_change end
676691
}
677692
}
678693
}

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import { useExit } from "./exit"
2727
import { useArgs } from "./args"
2828
import { batch, onMount } from "solid-js"
2929
import { Log } from "@/util/log"
30+
// altimate_change start - yolo mode
31+
import { Flag } from "@/flag/flag"
32+
// altimate_change end
3033
import type { Path } from "@opencode-ai/sdk"
3134
import type { Workspace } from "@opencode-ai/sdk/v2"
3235

@@ -136,6 +139,22 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
136139

137140
case "permission.asked": {
138141
const request = event.properties
142+
// altimate_change start - yolo mode: auto-approve without showing prompt
143+
if (Flag.ALTIMATE_CLI_YOLO) {
144+
sdk.client.permission
145+
.reply({
146+
requestID: request.id,
147+
reply: "once",
148+
})
149+
.catch((e) => {
150+
Log.Default.error("yolo mode auto-approve failed", {
151+
error: e instanceof Error ? e.message : String(e),
152+
requestID: request.id,
153+
})
154+
})
155+
break
156+
}
157+
// altimate_change end
139158
const requests = store.permission[request.sessionID]
140159
if (!requests) {
141160
setStore("permission", request.sessionID, [request])

packages/opencode/src/cli/cmd/tui/routes/session/footer.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { useDirectory } from "../../context/directory"
55
import { useConnected } from "../../component/dialog-model"
66
import { createStore } from "solid-js/store"
77
import { useRoute } from "../../context/route"
8+
// altimate_change start - yolo mode visual indicator
9+
import { Flag } from "@/flag/flag"
10+
// altimate_change end
811

912
export function Footer() {
1013
const { theme } = useTheme()
@@ -60,6 +63,13 @@ export function Footer() {
6063
</text>
6164
</Match>
6265
<Match when={connected()}>
66+
{/* altimate_change start - yolo mode visual indicator */}
67+
<Show when={Flag.ALTIMATE_CLI_YOLO}>
68+
<text fg={theme.warning}>
69+
<span style={{ fg: theme.warning }}></span> YOLO
70+
</text>
71+
</Show>
72+
{/* altimate_change end */}
6373
<Show when={permissions().length > 0}>
6474
<text fg={theme.warning}>
6575
<span style={{ fg: theme.warning }}></span> {permissions().length} Permission

packages/opencode/src/flag/flag.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export namespace Flag {
3636
// altimate_change start - opt-in for session-end auto-extraction
3737
export const ALTIMATE_MEMORY_AUTO_EXTRACT = altTruthy("ALTIMATE_MEMORY_AUTO_EXTRACT", "OPENCODE_MEMORY_AUTO_EXTRACT")
3838
// altimate_change end
39+
// altimate_change start - yolo mode: auto-approve all permission prompts
40+
// Declared here, defined via dynamic getter below (must evaluate at access time
41+
// because --yolo CLI flag sets the env var in middleware after module load)
42+
export declare const ALTIMATE_CLI_YOLO: boolean
43+
// altimate_change end
3944
// altimate_change start - opt-out for AI Teammate training system
4045
export const ALTIMATE_DISABLE_TRAINING = altTruthy("ALTIMATE_DISABLE_TRAINING", "OPENCODE_DISABLE_TRAINING")
4146
// altimate_change end
@@ -139,6 +144,23 @@ Object.defineProperty(Flag, "OPENCODE_CLIENT", {
139144
configurable: false,
140145
})
141146

147+
// altimate_change start - yolo mode: dynamic getter (set at runtime via --yolo flag)
148+
// ALTIMATE_CLI_YOLO is authoritative when defined; only falls back to OPENCODE_YOLO when undefined
149+
Object.defineProperty(Flag, "ALTIMATE_CLI_YOLO", {
150+
get() {
151+
const alt = process.env["ALTIMATE_CLI_YOLO"]
152+
if (alt !== undefined) {
153+
const v = alt.toLowerCase()
154+
return v === "true" || v === "1"
155+
}
156+
const oc = process.env["OPENCODE_YOLO"]?.toLowerCase()
157+
return oc === "true" || oc === "1"
158+
},
159+
enumerable: true,
160+
configurable: false,
161+
})
162+
// altimate_change end
163+
142164
// altimate_change start - ALTIMATE_CLI_CLIENT with OPENCODE_CLIENT fallback
143165
Object.defineProperty(Flag, "ALTIMATE_CLI_CLIENT", {
144166
get() {

packages/opencode/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ let cli = yargs(hideBin(process.argv))
7878
type: "string",
7979
choices: ["DEBUG", "INFO", "WARN", "ERROR"],
8080
})
81+
// altimate_change start - yolo mode as global flag
82+
.option("yolo", {
83+
describe: "auto-approve all permission prompts (explicit deny rules still enforced)",
84+
type: "boolean",
85+
default: false,
86+
})
87+
// altimate_change end
8188
.middleware(async (opts) => {
8289
await Log.init({
8390
print: process.argv.includes("--print-logs"),
@@ -96,6 +103,12 @@ let cli = yargs(hideBin(process.argv))
96103
process.env.DATAPILOT = "1"
97104
// altimate_change end
98105

106+
// altimate_change start - propagate --yolo flag to env var so Flag.ALTIMATE_CLI_YOLO picks it up
107+
if ("yolo" in opts && opts.yolo) {
108+
process.env.ALTIMATE_CLI_YOLO = "true"
109+
}
110+
// altimate_change end
111+
99112
// altimate_change start - telemetry init
100113
// Initialize telemetry early so events from MCP, engine, auth are captured.
101114
// init() is idempotent — safe to call again later in session prompt.

0 commit comments

Comments
 (0)