SLOPSHOPPER

auto-allow

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

newbandguardcommandtoaststatus
v0.1.0no licenseupdated 2026-10-23yu2001-s/auto-allow
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · auto-allow
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /auto-allow ⎿ auto-allow: Auto-allow is off (0 prompts approved this session); computer-use access cards: on. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

auto-allow

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.

What it does

  • Permission prompts: answers them with allow, except 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.
  • Computer-use access cards (macOS, Claude desktop app): while Claude's 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.
  • A drawer above the prompt: lists what it approved in this session (▸ N auto-approved), with a clear button.
  • A log: every prompt it sees goes to ~/.claude/auto-allow/log.jsonl (tool, target, decision; never file contents), last 2000 entries.

Commands

  • /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.

Computer-use cards: what they need

  • macOS, with the Claude desktop app allowed under System Settings → Privacy & Security → Accessibility. Computer use already asks for this.
  • The Mac unlocked, and the session open in the Claude window. Otherwise the card waits for you as usual, and a toast says why it couldn't answer.

Install

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.

Source 2 files
hooks/register.tsx 241 lines
1import { 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}
241
types/index.d.ts 17 lines
1export 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