Every shell command Claude asks permission for comes with a plain-English explanation and a risk level: low, medium or high.

When Claude asks permission to run a shell command, the question says in plain English what the command does and how risky it is, so you can answer without reading shell.
Deletes files and folders, including everything inside them. 🔴 high risk.
Runs a project script from package.json, then uploads your commits to the remote repository. 🟡 medium risk.
curl | bash, sudo, Docker, processes, services, remote copies, in-place edits, reads.a && b) lists each step and takes the highest risk.Only commands that are put to you are explained; commands that run without asking are left alone.
/config)| Option | Default | |
|---|---|---|
| Explain unknown commands with a small model | off | Ask a small model when the table does not know a command |
| Model | claude-haiku-5-5 | The model asked |
/plugin install jr-explain --marketplace jrlprost/claude-code-mods
Reads the command of Bash calls whose permission is being asked. With the model option on, sends unknown commands to the model through the session's own client. Writes nothing, runs nothing.
hooks/register.ts 37 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import type { Explanation } from './explain'
4import { explainCommand, parseModelAnswer, reasonFor } from './explain'
5
6const cache = new Map<string, Explanation | null>()
7
8/** A small model's explanation, for commands the table does not know. One line, cached per command. */
9async function askModel($: EngineInterface, model: string, command: string): Promise<Explanation | null> {
10 if (cache.has(command)) return cache.get(command) ?? null
11 const answer = await $.model.complete({
12 model,
13 system: 'You explain shell commands to a non-developer. Answer with one short plain-English clause saying what the command does, then " | ", then its risk: low, medium or high. Nothing else.',
14 prompt: command.slice(0, 2000),
15 maxTokens: 60,
16 timeoutMs: 5000,
17 })
18 const explanation = answer.isAnswered ? parseModelAnswer(answer.text) : null
19 cache.set(command, explanation)
20 return explanation
21}
22
23export const register: Register = (on, options) => {
24 const useModel = options.useModel === true
25 const model = String(options.model ?? 'claude-haiku-5-5')
26
27 // When a Bash call is about to be put to you, the question carries a plain-English explanation.
28 on('tool.check', { tool: 'Bash' }, async ($, e, next) => {
29 const verdict = await next(e)
30 if (verdict.decision !== 'ask') return verdict
31 const command = (e.input as { command?: string }).command ?? ''
32 const explanation = explainCommand(command) ?? (useModel ? await askModel($, model, command) : null)
33 if (!explanation) return verdict
34 return { ...verdict, reason: reasonFor(explanation, verdict.reason) }
35 })
36}
37hooks/explain.ts 65 lines1// Plain-English explanations of shell commands, from a table first.
2
3export type Risk = 'low' | 'medium' | 'high'
4export type Explanation = { what: string; risk: Risk }
5
6const TABLE: { test: RegExp; what: string; risk: Risk }[] = [
7 { test: /\b(curl|wget)\b[^|]*\|\s*(sudo\s+)?(ba|z)?sh\b/, what: 'downloads a script from the internet and runs it', risk: 'high' },
8 { test: /\bsudo\b/, what: 'runs a command as administrator', risk: 'high' },
9 { test: /\brm\s+(-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\b/, what: 'deletes files and folders, including everything inside them', risk: 'high' },
10 { test: /\bgit\s+push\b[^;&|]*(\s-f\b|--force)/, what: 'overwrites the remote branch history with yours', risk: 'high' },
11 { test: /\bgit\s+reset\s+--hard\b/, what: 'throws away uncommitted changes', risk: 'high' },
12 { test: /\bgit\s+clean\s+-[a-zA-Z]*f/, what: 'deletes untracked files', risk: 'high' },
13 { test: /\bgit\s+(checkout|restore)\s+(--\s+)?\./, what: 'throws away uncommitted edits in the working tree', risk: 'high' },
14 { test: /\b(dd|mkfs|diskutil\s+erase)\b/, what: 'writes directly to a disk', risk: 'high' },
15 { test: /\b(drop\s+(table|database)|truncate\s+table)\b/i, what: 'deletes database tables or data', risk: 'high' },
16 { test: /\bchmod\s+-R\b|\bchown\s+-R\b/, what: 'changes permissions on a whole folder tree', risk: 'medium' },
17 { test: /\brm\b/, what: 'deletes files', risk: 'medium' },
18 { test: /\bgit\s+push\b/, what: 'uploads your commits to the remote repository', risk: 'medium' },
19 { test: /\bgit\s+(rebase|merge|cherry-pick)\b/, what: 'rewrites or combines branch history', risk: 'medium' },
20 { test: /\bgit\s+commit\b/, what: 'records the staged changes as a commit', risk: 'low' },
21 { test: /\bgit\s+(status|log|diff|show|branch|fetch)\b/, what: 'reads the git history and state', risk: 'low' },
22 { test: /\b(npm|pnpm|yarn|bun)\s+(install|i|add|ci)\b|\bpip3?\s+install\b|\buv\s+add\b|\bcargo\s+add\b|\bgem\s+install\b|\bbrew\s+install\b/, what: 'installs packages, which may run their own install scripts', risk: 'medium' },
23 { test: /\bnpx\b|\bpnpm\s+dlx\b|\buvx\b|\bbunx\b/, what: 'downloads a package and runs it', risk: 'medium' },
24 { test: /\b(npm|pnpm|yarn|bun)\s+(run\s+)?(test|build|lint|dev|start|typecheck)\b/, what: 'runs a project script from package.json', risk: 'low' },
25 { test: /\bdocker\s+(system\s+prune|volume\s+rm|rm\s+-f)\b/, what: 'deletes Docker containers, volumes or caches', risk: 'high' },
26 { test: /\bdocker\s+(run|compose\s+up|build)\b/, what: 'builds or starts containers', risk: 'medium' },
27 { test: /\b(kill|pkill|killall)\b/, what: 'stops running processes', risk: 'medium' },
28 { test: /\b(launchctl|crontab|systemctl)\b/, what: 'changes background services or scheduled jobs', risk: 'medium' },
29 { test: /\b(ssh|scp|rsync)\b/, what: 'connects to or copies files to another machine', risk: 'medium' },
30 { test: /\bsed\s+-i\b|\bperl\s+-pi\b/, what: 'edits files in place', risk: 'medium' },
31 { test: /\bmv\b/, what: 'moves or renames files', risk: 'medium' },
32 { test: /\b(curl|wget|http)\b/, what: 'fetches a web address', risk: 'low' },
33 { test: /\b(cp|mkdir|touch)\b/, what: 'copies files or creates folders', risk: 'low' },
34 { test: /\b(ls|cat|head|tail|grep|rg|find|wc|pwd|echo|which|stat|du|df)\b/, what: 'reads files or folders', risk: 'low' },
35 { test: /\b(python3?|node|ruby|bash|sh|zsh)\s+\S+/, what: 'runs a script', risk: 'medium' },
36]
37
38const RANK: Record<Risk, number> = { low: 0, medium: 1, high: 2 }
39
40const segments = (command: string) => command.split(/&&|\|\||;|\n/).map(s => s.trim()).filter(Boolean)
41
42/** One explanation per part of a compound command, then the highest risk of them. */
43export const explainCommand = (command: string): Explanation | null => {
44 const found: Explanation[] = []
45 for (const part of segments(command)) {
46 const row = TABLE.find(r => r.test.test(part))
47 if (row && !found.some(f => f.what === row.what)) found.push({ what: row.what, risk: row.risk })
48 }
49 if (found.length === 0) return null
50 const risk = found.reduce<Risk>((max, f) => (RANK[f.risk] > RANK[max] ? f.risk : max), 'low')
51 const what = found.slice(0, 3).map(f => f.what)
52 const sentence = what.length === 1 ? what[0]! : `${what.slice(0, -1).join(', ')}, then ${what[what.length - 1]}`
53 return { what: sentence.charAt(0).toUpperCase() + sentence.slice(1), risk }
54}
55
56export const RISK_MARK: Record<Risk, string> = { low: '🟢 low risk', medium: '🟡 medium risk', high: '🔴 high risk' }
57
58export const reasonFor = (e: Explanation, previous?: string) => `${e.what}. ${RISK_MARK[e.risk]}.${previous ? ` (${previous})` : ''}`
59
60/** Reads the model's one-line answer: "<explanation> | <low|medium|high>". */
61export const parseModelAnswer = (text: string): Explanation | null => {
62 const m = text.trim().match(/^(.+?)\s*\|\s*(low|medium|high)\s*$/i)
63 return m ? { what: m[1]!.replace(/\.$/, ''), risk: m[2]!.toLowerCase() as Risk } : null
64}
65