Auto-approves permission prompts that still appear in bypass mode, except for a short list of catastrophic commands

A Claude Code mod that answers the permission prompts that still show up in bypass mode, so a long run doesn't stall waiting for a click. It also approves the Claude desktop app's computer-use "Request Access" card, which no permission hook can see.
It removes a human check. Everything it approves would otherwise have waited for you. Install it only where you already run with bypass permissions and are comfortable with that.
AskUserQuestion, ExitPlanMode and a short list of destructive Bash commands (rm -rf / or ~, mkfs, dd to a disk, force-push, a fork bomb), which still get the normal prompt.request_access call waits, a helper script finds the card showing that call's reason and presses its ⌘↩ (Allow for this session). If a draft is waiting in the prompt box, or the shortcut does nothing, it clicks the button instead and then puts the pointer and the front app back.▸ N auto-approved), with a clear button.~/.claude/auto-allow/log.jsonl (tool, target, decision; never file contents), last 2000 entries./auto-allow on | off | status: switch it, or see what it approved this session./auto-allow computer on | off: switch the computer-use card part alone. It is on by default./auto-allow log: the last 20 entries of the log.At the prompt of a terminal session:
/plugin install auto-allow --marketplace yu2001-s/auto-allow
Answer y to add the marketplace, then pick a scope. The user scope is first.
From a shell:
claude plugin install auto-allow --marketplace yu2001-s/auto-allow
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins. Choose Add marketplace with yu2001-s/auto-allow, then Add plugin.
hooks/register.tsx 241 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { AutoApproved } from '../types'
5
6const isOn = atom({ plugin: 'auto-allow', key: 'isOn' } as const, true)
7const allowed = atom({ plugin: 'auto-allow', key: 'allowed' } as const, 0)
8const recent = atom({ plugin: 'auto-allow', key: 'recent' } as const, [] as AutoApproved[])
9const isDrawerOpen = atom({ plugin: 'auto-allow', key: 'isDrawerOpen' } as const, false)
10const isComputerUseOn = atom({ plugin: 'auto-allow', key: 'isComputerUseOn' } as const, true)
11
12// How many approvals the drawer keeps, and shows when open.
13const KEEP = 50
14const SHOW = 8
15
16// Prompts that are questions for you, not permission checks: auto-answering them makes no sense.
17const NEVER_AUTO = new Set(['AskUserQuestion', 'ExitPlanMode'])
18
19// Bash commands that still get the normal prompt, even with auto-allow on.
20const DANGEROUS = [
21 /\brm\s+-[a-z]*r[a-z]*f?[a-z]*\s+(\/|~|\$HOME)(\s|$)/i, // rm -rf / or ~
22 /\bmkfs\b|\bdd\s+.*\bof=\/dev\//, // wiping disks
23 /\bgit\s+push\b.*(--force|-f\b)/, // force-push
24 /:\(\)\s*\{\s*:\|:&\s*\};:/, // fork bomb
25]
26
27// Computer-use access is asked on a card of the desktop app's own, which no permission hook
28// sees: a helper waits for the card showing the call's reason and presses its ⌘↩ (Allow for
29// this session). It prints `approved <how>` or `failed <why>`, and ticks on stderr while it waits.
30const CU_ACCESS = 'mcp__computer-use__request_access'
31const APPROVE_SCRIPT = 'scripts/approve-access-card.js'
32const HOW_APPROVED: Record<string, string> = {
33 shortcut: 'pressed ⌘↩ on its card',
34 click: 'clicked Allow on its card',
35}
36
37// Every prompt the mod saw, one JSON line each, kept to the last MAX_LINES.
38const LOG_PATH = '.claude/auto-allow/log.jsonl'
39const MAX_LINES = 2000
40
41type LogEntry = {
42 time: string
43 session: string
44 cwd: string
45 tool: string
46 what: string
47 decision: 'allowed' | 'asked-you'
48 why?: string
49}
50
51// What the prompt was about, short and without file contents.
52const summarize = (input: unknown): string => {
53 const i = (input ?? {}) as Record<string, unknown>
54 const pick = i.command ?? i.file_path ?? i.notebook_path ?? i.url ?? i.path
55 const text = typeof pick === 'string' ? pick : JSON.stringify(input ?? {})
56 return text.length > 300 ? `${text.slice(0, 300)}…` : text
57}
58
59const logFile = async ($: EngineInterface) => `${(await $.env.get('HOME')) ?? '~'}/${LOG_PATH}`
60
61const readLog = async ($: EngineInterface): Promise<LogEntry[]> => {
62 const path = await logFile($)
63 if (!(await $.fs.exists(path))) return []
64 const text = await $.fs.read(path)
65 return text
66 .split('\n')
67 .filter(Boolean)
68 .map(line => JSON.parse(line) as LogEntry)
69}
70
71// $.fs has no append: writes run one after another so two prompts can't drop each other's line.
72let writing: Promise<void> = Promise.resolve()
73const writeLog = ($: EngineInterface, entry: LogEntry) => {
74 writing = writing
75 .then(async () => {
76 const lines = [...(await readLog($)), entry].slice(-MAX_LINES)
77 await $.fs.write(await logFile($), `${lines.map(l => JSON.stringify(l)).join('\n')}\n`)
78 })
79 .catch(() => {})
80 return writing
81}
82
83// Counts an approval, lists it in the drawer and logs it.
84const noteApproved = async ($: EngineInterface, entry: Omit<LogEntry, 'decision'>) => {
85 await update($, allowed, v => v + 1)
86 await update($, recent, list => [...list, { time: entry.time, tool: entry.tool, what: entry.what }].slice(-KEEP))
87 await writeLog($, { ...entry, decision: 'allowed' })
88}
89
90export const register: Register = on => {
91 on('session.start', async ($, e, next) => {
92 await $.command.register({
93 name: 'auto-allow',
94 description: 'Toggle auto-approval of permission prompts (on | off | status | log | computer on|off)',
95 })
96 $.ui.status(undefined)
97
98 return next(e)
99 })
100
101 on('command.run', { command: 'auto-allow' }, async ($, e) => {
102 const arg = (e.args ?? '').trim().toLowerCase()
103
104 if (arg === 'log') {
105 const recent = (await readLog($)).slice(-20)
106 if (recent.length === 0) return { text: 'Auto-allow log is empty so far.' }
107 const rows = recent.map(
108 l => `${l.time.slice(5, 16).replace('T', ' ')} ${l.decision === 'allowed' ? '✓' : '?'} ${l.tool} ${l.what}${l.why ? ` (${l.why})` : ''}`,
109 )
110 return { text: [`Last ${recent.length} prompts (full log: ~/${LOG_PATH}):`, ...rows].join('\n') }
111 }
112
113 const [word, value] = arg.split(/\s+/)
114 if (word === 'computer') await update($, isComputerUseOn, v => (value === 'on' ? true : value === 'off' ? false : !v))
115 else if (arg === 'on' || arg === 'off') await update($, isOn, () => arg === 'on')
116 else if (arg !== 'status') await update($, isOn, v => !v)
117
118 const now = await read($, isOn)
119 const n = await read($, allowed)
120 const cards = await read($, isComputerUseOn)
121
122 return {
123 text: `Auto-allow is ${now ? 'on' : 'off'} (${n} prompts approved this session); computer-use access cards: ${cards ? 'on' : 'off'}.`,
124 }
125 })
126
127 // A drawer above the prompt: what auto-allow approved in this session, with Clear.
128 // Shown only while it has something to list; it steps aside while a survey holds the band.
129 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
130 const list = await read($, recent)
131 if (e.props.hasSurvey || list.length === 0) return next(e)
132
133 const { Box, Text, Button } = $.ui.resolve(e)
134 const isOpen = await read($, isDrawerOpen)
135 const shown = list.slice(-SHOW).reverse()
136 const hidden = list.length - shown.length
137
138 return (
139 <Box flexDirection="column">
140 <Box flexDirection="row" gap={2}>
141 <Button key="drawer" plain dimColor onPress={() => update($, isDrawerOpen, v => !v)}>
142 {`${isOpen ? '▾' : '▸'} ${list.length} auto-approved`}
143 </Button>
144 <Button
145 key="clear"
146 plain
147 dimColor
148 onPress={async () => {
149 await update($, recent, () => [])
150 await update($, isDrawerOpen, () => false)
151 }}
152 >
153 clear
154 </Button>
155 </Box>
156 {isOpen &&
157 shown.map(item => (
158 <Text dimColor wrap="truncate-end">
159 {` ${item.time.slice(11, 16)} ${item.tool} ${item.what}`}
160 </Text>
161 ))}
162 {isOpen && hidden > 0 && <Text dimColor>{` +${hidden} more: /auto-allow log`}</Text>}
163 </Box>
164 )
165 })
166
167 on('classic.PermissionRequest', async ($, e, next) => {
168 const base = {
169 time: new Date().toISOString(),
170 session: e.session_id,
171 cwd: e.cwd,
172 tool: e.tool_name,
173 what: summarize(e.tool_input),
174 }
175 const askYou = async (why: string) => {
176 await writeLog($, { ...base, decision: 'asked-you', why })
177 return next(e)
178 }
179
180 try {
181 if (!(await read($, isOn))) return askYou('auto-allow off')
182 if (NEVER_AUTO.has(e.tool_name)) return askYou('needs your answer')
183
184 const command = (e.tool_input as { command?: unknown } | undefined)?.command
185 if (typeof command === 'string' && DANGEROUS.some(re => re.test(command))) {
186 $.ui.toast(`Auto-allow skipped: ${command.slice(0, 60)}`)
187 return askYou('dangerous command')
188 }
189
190 await noteApproved($, base)
191
192 return { decision: { behavior: 'allow' } }
193 } catch {
194 // If anything goes wrong, fall back to the normal prompt.
195 return next(e)
196 }
197 })
198
199 // While a computer-use access call waits on its card, the helper presses it. Once the call
200 // settles the loop leaves at the helper's next tick, which kills it.
201 on('tool.call', { tool: CU_ACCESS }, async ($, e, next) => {
202 if (!(await read($, isOn)) || !(await read($, isComputerUseOn))) return next(e)
203
204 const reason = typeof e.reason === 'string' ? e.reason : ''
205 const apps = Array.isArray(e.apps) ? e.apps.filter(app => typeof app === 'string').join(', ') : ''
206 const helper = $.process.spawn({ argv: ['osascript', '-l', 'JavaScript', `${$.plugin.root}/${APPROVE_SCRIPT}`, reason] })
207 let isSettled = false
208 const pressing = (async () => {
209 let said = ''
210 for await (const { stream, text } of helper) {
211 if (stream === 'stdout') said += text
212 else if (isSettled) break
213 }
214 return said.trim()
215 })().catch(() => 'failed its helper did not start')
216 void pressing.then(said => {
217 if (said.startsWith('failed ')) $.ui.toast(`Auto-allow couldn't answer the computer-use card (${said.slice(7)}): answer it yourself`)
218 })
219
220 try {
221 return await next(e)
222 } finally {
223 isSettled = true
224 const said = await pressing
225 const how = said.match(/^approved (\w+)/)?.[1]
226 // Noting it down never changes what the call answered.
227 try {
228 const base = {
229 time: new Date().toISOString(),
230 session: await $.session.id(),
231 cwd: await $.session.cwd(),
232 tool: 'computer use',
233 what: apps,
234 }
235 if (how) await noteApproved($, { ...base, why: HOW_APPROVED[how] ?? how })
236 else if (said.startsWith('failed ')) await writeLog($, { ...base, decision: 'asked-you', why: said.slice(7) })
237 } catch {}
238 }
239 })
240}
241types/index.d.ts 17 lines1export type AutoAllowCount = number
2
3/** One prompt the mod approved in this session, as the drawer lists it. */
4export type AutoApproved = { time: string; tool: string; what: string }
5
6declare module 'claude-code' {
7 interface PluginState {
8 'auto-allow': {
9 isOn: boolean
10 allowed: AutoAllowCount
11 recent: AutoApproved[]
12 isDrawerOpen: boolean
13 isComputerUseOn: boolean
14 }
15 }
16}
17