Skip to content
Open
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
10 changes: 10 additions & 0 deletions packages/opencode/src/permission/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ export namespace Permission {

if (!needsAsk) return

// In non-interactive/headless mode (no TTY, e.g. GitHub Actions), auto-reject
// permission requests that would normally block waiting for user input
if (!process.stdout.isTTY) {
log.info("noninteractive mode (no TTY), rejecting permission request", {
permission: request.permission,
patterns: request.patterns,
})
return yield* new RejectedError()
}

const id = request.id ?? PermissionID.ascending()
const info: Request = {
id,
Expand Down
37 changes: 37 additions & 0 deletions packages/opencode/test/permission/next.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1146,3 +1146,40 @@ test("ask - abort should clear pending request", async () => {
},
})
})

test("ask - rejects immediately in non-interactive mode without blocking", async () => {
// Save original TTY state and simulate non-interactive (no TTY)
const originalIsTTY = process.stdout.isTTY
process.stdout.isTTY = false

try {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const start = Date.now()
// In non-interactive mode (no TTY), "ask" should immediately reject
const err = await Permission.ask({
sessionID: SessionID.make("session_noninteractive"),
permission: "bash",
patterns: ["ls"],
metadata: {},
always: [],
ruleset: [{ permission: "bash", pattern: "*", action: "ask" }],
}).then(
() => undefined,
(e) => e,
)

// Should reject immediately (within 1 second, not hang)
expect(Date.now() - start).toBeLessThan(1000)
expect(err).toBeInstanceOf(Permission.RejectedError)
// No pending requests should be created
expect(await Permission.list()).toHaveLength(0)
},
})
} finally {
// Restore original TTY state
process.stdout.isTTY = originalIsTTY
}
})
5 changes: 5 additions & 0 deletions packages/opencode/test/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import fs from "fs/promises"
import { setTimeout as sleep } from "node:timers/promises"
import { afterAll } from "bun:test"

// Mock TTY to true for tests (simulates having a terminal)
// Tests that need to test non-interactive behavior should override this within their test
process.stdout.isTTY = true
process.stderr.isTTY = true

// Set XDG env vars FIRST, before any src/ imports
const dir = path.join(os.tmpdir(), "opencode-test-data-" + process.pid)
await fs.mkdir(dir, { recursive: true })
Expand Down
Loading