SLOPSHOPPER

scoped-approve

Claude Code skill that builds, debugs, audits and packages Claude Code mods from a plain-language description

newguardcommand
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · scoped-approve
› 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 › /approvals ⎿ scoped-approve: Allow-list is empty: nothing is auto-approved. ⎿ scoped-approve: Nothing approved this session. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

scoped-approve

Leia em português

Lets a short list of commands you trust run without the permission question, and nothing else.

What is a mod?

Claude Code is Anthropic's assistant for working with code, in the terminal or in the Claude desktop app. A mod is a small add-on for it: it can add a panel, a bar above the box where you type, a new /command, or a rule that checks what Claude is about to do. This folder is one complete mod you can install and use as it is, or copy as a starting point.

What it does

By default Claude Code asks before running a command. That is safe, but answering "yes" to npm test for the tenth time gets old. This mod lets you write a list of commands that may run without asking. Anything not on the list gets the normal question, exactly as before.

What you see

  • When a command runs because it is on your list, a faint line in the conversation says so: auto-approved by allow-list entry "npm test *": npm test -- --watch.
  • /approvals shows your list and what was approved in this session.

How to use it

  1. In the settings, write the commands you trust in Allowed commands, separated by commas. Example: git status, npm test *.
  2. An entry matches only that exact command. Add a space and * at the end to also accept anything after it: npm test * allows npm test and npm test -- --watch.
  3. Type /approvals to check what is on the list.
  4. To pause it without uninstalling, turn off Enabled.

Keyboard shortcuts

None of its own.

Commands

TypeWhat it does
/approvalsShow the list and the approvals of this session

Settings

Claude Code asks for these when you enable the mod. To change them later, open /config and find the mod by name.

SettingWhat it meansDefault
allowList (Allowed commands)Commands that may run without asking, separated by commas. End one with a space and * to accept arguments.(empty)
enabled (Enabled)Turn the mod off without uninstalling it.true

Good to know

  • Commands joined with ;, &&, |, redirections or $( ) are never approved automatically, even if they start with an allowed command.
  • It never overrides a "deny" from your own permission rules and never approves for subagents (helper agents Claude starts).
  • Every entry is a command Claude may run without asking you. Keep the list short and exact.

Install

You need Claude Code 2.1.287 or newer in the terminal, or 2.1.286 or newer in the desktop app.

  1. Copy this folder anywhere on your computer.
  2. Inside the copy, rename the folder _claude-plugin to .claude-plugin (with a dot at the start). It ships without the dot so the examples never load by accident.
  3. Start Claude Code with the mod:
claude --plugin-dir path/to/scoped-approve

To keep it installed for good, ask the claude-mod-forge skill to package it ("package this mod").

Check it

From the skill folder (skills/claude-mod-forge), this runs the manifest check, a strict type check and the 8 automated tests of this mod:

node scripts/check-mod.mjs examples/scoped-approve

The tests cover the mod's logic and what it draws in the terminal and the desktop app. How it looks in your own window is worth a quick look in a real session.

For developers: the examples table lists the hooks and API calls this mod demonstrates.

Source 3 files
hooks/register.ts 51 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { allowedBy, parseList } from './allow'
5
6// An APPROVER, not a guard. Rules it never breaks:
7// - it only turns the engine's "ask" into "allow": a deny stays a deny, an allow stays an allow;
8// - only the main conversation (no agentId): subagents keep the normal prompts;
9// - on failure it returns the engine's own verdict (never allow, never deny-everything).
10const approvals = atom({ plugin: 'scoped-approve', key: 'approvals' } as const, [])
11
12export const register: Register = (on, options) => {
13  const list = parseList(String(options.allowList ?? ''))
14  const isEnabled = options.enabled !== false
15
16  on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
17    const decided = await next(e)
18    if (!isEnabled || decided.decision !== 'ask' || e.agentId !== undefined) return decided
19
20    const command = String((e.input as { command?: unknown }).command ?? '')
21    const entry = allowedBy(command, list)
22    if (entry === undefined) return decided
23
24    // Every approval is visible: a dim transcript line, and the session list behind /approvals.
25    $.ui.log(`auto-approved by allow-list entry "${entry}": ${command.slice(0, 120)}`)
26    const at = await $.clock.now()
27    await update($, approvals, l => [...l, { command: command.slice(0, 200), at }].slice(-50))
28    return { decision: 'allow', reason: `scoped-approve: allow-list entry "${entry}"` }
29  }).catch(($, e, next) => next(e))
30
31  on('session.start', async ($, e, next) => {
32    try {
33      await $.command.register({ name: 'approvals', description: 'Show the allow-list and what it approved this session', immediate: true })
34    } catch {
35      // name taken: approvals still work and still log
36    }
37    return next(e)
38  })
39
40  on('command.run', { command: 'approvals' }, async $ => {
41    const done = await read($, approvals)
42    const header = !isEnabled
43      ? 'scoped-approve is off.'
44      : list.length === 0
45        ? 'Allow-list is empty: nothing is auto-approved.'
46        : `Allow-list: ${list.join(' · ')}`
47    const body = done.length === 0 ? 'Nothing approved this session.' : done.map(a => `- ${a.command}`).join('\n')
48    return { text: `${header}\n${body}` }
49  })
50}
51
hooks/allow.ts 34 lines
1// Pure allow-list logic. No $ here, so tests cover it directly.
2
3// Anything that chains, substitutes or redirects is never auto-approved:
4// "git status && rm -rf x" must not ride along with "git status".
5const COMPOUND = /[;&|`<>\n\r]|\$\(/
6
7// An entry is EXACT by default: "git diff" approves "git diff" and nothing else, because arguments can
8// change what a command does ("git diff --output=FILE" overwrites a file). Arguments are accepted only
9// when the entry says so with a trailing " *": "npm test *" approves "npm test" and "npm test -- foo".
10// A "*" anywhere else, or an entry that is only "*", is dropped: it would approve far more than it says.
11const ARGS = ' *'
12
13export function parseList(raw: string): string[] {
14  return raw
15    .split(',')
16    .map(s => s.trim().replace(/\s+/g, ' '))
17    .filter(s => s.length > 0 && !COMPOUND.test(s))
18    .filter(s => {
19      const base = s.endsWith(ARGS) ? s.slice(0, -ARGS.length) : s
20      return base.length > 0 && !base.includes('*')
21    })
22}
23
24// Returns the entry that allows the command, or undefined.
25export function allowedBy(command: string, list: readonly string[]): string | undefined {
26  const cmd = command.trim().replace(/\s+/g, ' ')
27  if (cmd === '' || COMPOUND.test(cmd)) return undefined
28  return list.find(entry => {
29    if (!entry.endsWith(ARGS)) return cmd === entry
30    const base = entry.slice(0, -ARGS.length)
31    return cmd === base || cmd.startsWith(`${base} `)
32  })
33}
34
types/index.d.ts 8 lines
1export type Approval = { command: string; at: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'scoped-approve': { approvals: Approval[] }
6  }
7}
8