Skip to content

Commit 09e1999

Browse files
groeimetaiclaude
andcommitted
feat: add --dangerously-skip-permissions flag and plan exit feedback UX
Add --dangerously-skip-permissions CLI flag (and SNOW_CODE_DANGEROUSLY_SKIP_PERMISSIONS env var) to auto-approve all permission and question prompts. Also allow users to provide custom feedback when declining plan/review exit, so the agent can refine instead of halting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 05ae65b commit 09e1999

File tree

7 files changed

+66
-2
lines changed

7 files changed

+66
-2
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,21 @@ export const RunCommand = cmd({
9191
type: "string",
9292
describe: "model variant (provider-specific reasoning effort, e.g., high, max, minimal)",
9393
})
94+
.option("dangerously-skip-permissions", {
95+
type: "boolean",
96+
describe: "auto-approve all permission and question prompts (dangerous)",
97+
})
9498
},
9599
handler: async (args) => {
100+
if (args.dangerouslySkipPermissions) {
101+
process.env.SNOW_CODE_DANGEROUSLY_SKIP_PERMISSIONS = "true"
102+
UI.println(
103+
UI.Style.TEXT_DANGER_BOLD + "!",
104+
UI.Style.TEXT_NORMAL,
105+
"Running with --dangerously-skip-permissions: ALL permissions and questions will be auto-approved",
106+
)
107+
}
108+
96109
let message = [...args.message, ...(args["--"] || [])]
97110
.map((arg) => (arg.includes(" ") ? `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : arg))
98111
.join(" ")
@@ -209,6 +222,14 @@ export const RunCommand = cmd({
209222
if (event.type === "permission.asked") {
210223
const permission = event.properties
211224
if (permission.sessionID !== sessionID) continue
225+
if (args.dangerouslySkipPermissions) {
226+
await sdk.permission.respond({
227+
sessionID,
228+
permissionID: permission.id,
229+
response: "once",
230+
})
231+
continue
232+
}
212233
const result = await select({
213234
message: `Permission required: ${permission.permission} (${permission.patterns.join(", ")})`,
214235
options: [

packages/opencode/src/cli/cmd/tui/thread.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ export const TuiThreadCommand = cmd({
7575
.option("connect", {
7676
type: "string",
7777
describe: "connect to an existing server URL instead of starting a worker",
78+
})
79+
.option("dangerously-skip-permissions", {
80+
type: "boolean",
81+
describe: "auto-approve all permission and question prompts (dangerous)",
7882
}),
7983
handler: async (args) => {
8084
// Resolve relative paths against PWD to preserve behavior when using --cwd flag
@@ -94,6 +98,15 @@ export const TuiThreadCommand = cmd({
9498
return piped ? piped + "\n" + args.prompt : args.prompt
9599
})
96100

101+
if (args.dangerouslySkipPermissions) {
102+
process.env.SNOW_CODE_DANGEROUSLY_SKIP_PERMISSIONS = "true"
103+
UI.println(
104+
UI.Style.TEXT_DANGER_BOLD + "!",
105+
UI.Style.TEXT_NORMAL,
106+
"Running with --dangerously-skip-permissions: ALL permissions and questions will be auto-approved",
107+
)
108+
}
109+
97110
let url: string
98111
let customFetch: typeof fetch | undefined
99112
let events: EventSource | undefined

packages/opencode/src/flag/flag.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export namespace Flag {
3636
export const OPENCODE_DISABLE_CLAUDE_CODE_SKILLS =
3737
OPENCODE_DISABLE_CLAUDE_CODE || truthyBoth("DISABLE_CLAUDE_CODE_SKILLS")
3838
export declare const OPENCODE_DISABLE_PROJECT_CONFIG: boolean
39+
export declare const OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS: boolean
3940
export const OPENCODE_FAKE_VCS = envBoth("FAKE_VCS")
4041
export const OPENCODE_CLIENT = envBoth("CLIENT") ?? "cli"
4142
export const OPENCODE_SERVER_PASSWORD = envBoth("SERVER_PASSWORD")
@@ -67,6 +68,16 @@ export namespace Flag {
6768
}
6869
}
6970

71+
// Dynamic getter for OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS
72+
// Must be evaluated at access time because the TUI handler sets the env var after module load
73+
Object.defineProperty(Flag, "OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS", {
74+
get() {
75+
return truthy("SNOW_CODE_DANGEROUSLY_SKIP_PERMISSIONS") || truthy("OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS")
76+
},
77+
enumerable: true,
78+
configurable: false,
79+
})
80+
7081
// Dynamic getter for OPENCODE_DISABLE_PROJECT_CONFIG
7182
// This must be evaluated at access time, not module load time,
7283
// because external tooling may set this env var at runtime

packages/opencode/src/permission/next.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Bus } from "@/bus"
22
import { BusEvent } from "@/bus/bus-event"
33
import { Config } from "@/config/config"
4+
import { Flag } from "@/flag/flag"
45
import { Identifier } from "@/id/id"
56
import { Instance } from "@/project/instance"
67
import { Storage } from "@/storage/storage"
@@ -137,6 +138,10 @@ export namespace PermissionNext {
137138
if (rule.action === "deny")
138139
throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission)))
139140
if (rule.action === "ask") {
141+
if (Flag.OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS) {
142+
log.warn("auto-approving permission", { permission: request.permission, pattern })
143+
continue
144+
}
140145
const id = input.id ?? Identifier.ascending("permission")
141146
return new Promise<void>((resolve, reject) => {
142147
const info: Request = {

packages/opencode/src/question/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Bus } from "@/bus"
22
import { BusEvent } from "@/bus/bus-event"
3+
import { Flag } from "@/flag/flag"
34
import { Identifier } from "@/id/id"
45
import { Instance } from "@/project/instance"
56
import { Log } from "@/util/log"
@@ -99,6 +100,11 @@ export namespace Question {
99100
questions: Info[]
100101
tool?: { messageID: string; callID: string }
101102
}): Promise<Answer[]> {
103+
if (Flag.OPENCODE_DANGEROUSLY_SKIP_PERMISSIONS) {
104+
log.warn("auto-approving question", { questions: input.questions.length })
105+
return input.questions.map((q) => [q.options[0]?.label ?? "Yes"])
106+
}
107+
102108
const s = await state()
103109
const id = Identifier.ascending("question")
104110

@@ -165,6 +171,12 @@ export namespace Question {
165171
}
166172
}
167173

174+
export class FeedbackError extends Error {
175+
constructor(feedback: string) {
176+
super(`The user wants changes to the plan: ${feedback}`)
177+
}
178+
}
179+
168180
export async function list() {
169181
return state().then((x) => Object.values(x.pending).map((x) => x.info))
170182
}

packages/opencode/src/tool/plan.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const PlanExitTool = Tool.define("plan_exit", {
2929
{
3030
question: `Plan at ${plan} is complete. Would you like to switch to the build agent and start implementing?`,
3131
header: "Build Agent",
32-
custom: false,
32+
custom: true,
3333
options: [
3434
{ label: "Yes", description: "Switch to build agent and start implementing the plan" },
3535
{ label: "No", description: "Stay with plan agent to continue refining the plan" },
@@ -41,6 +41,7 @@ export const PlanExitTool = Tool.define("plan_exit", {
4141

4242
const answer = answers[0]?.[0]
4343
if (answer === "No") throw new Question.RejectedError()
44+
if (answer !== "Yes") throw new Question.FeedbackError(answer ?? "")
4445

4546
const model = await getLastModel(ctx.sessionID)
4647

packages/opencode/src/tool/review.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const ReviewExitTool = Tool.define("review_exit", {
2929
{
3030
question: `Review at ${reviewFile} is complete. Would you like to switch to the build agent and start acting on the feedback?`,
3131
header: "Build Agent",
32-
custom: false,
32+
custom: true,
3333
options: [
3434
{ label: "Yes", description: "Switch to build agent and act on review feedback" },
3535
{ label: "No", description: "Stay with review agent to continue analysis" },
@@ -41,6 +41,7 @@ export const ReviewExitTool = Tool.define("review_exit", {
4141

4242
const answer = answers[0]?.[0]
4343
if (answer === "No") throw new Question.RejectedError()
44+
if (answer !== "Yes") throw new Question.FeedbackError(answer ?? "")
4445

4546
const model = await getLastModel(ctx.sessionID)
4647

0 commit comments

Comments
 (0)