Holds destructive or unrequested shell commands unless the user asked for them in this session

Great software no longer has to cost a fortune or consume every hour of your day. With Claude Code, you get an engineering partner that plans, builds, tests, and ships alongside you at a fraction of the time and cost. This isn't about prompting an AI. It's about designing systems that multiply what you can deliver. Welcome to the new standard.
Agentic software engineering is a discipline shift, and once you experience it, there's no going back. Instead of spending days on tasks that take agents minutes, you orchestrate workflows that reduce cost, eliminate repetition, and scale your output without scaling your team. The developers who master this don't just move faster. They do more with less, and they do it consistently.
Across 11 hands-on modules, you'll go from Claude Code fundamentals to production-grade multi-agent pipelines. You'll design composable skills, build MCP tools that connect agents to real systems, architect parallel sub-agent workflows, and harden everything for real-world conditions. Every module is built around genuine engineering decisions, because the goal was never to write more code. It was always to build software that matters.
| Module | Description |
|---|---|
| 01 Fundamentals | Claude product family, installation and authentication, model selection and prompt caching |
| 02 Harness | Settings hierarchy, CLAUDE.md constitution, custom slash commands, and event-driven hooks |
| 03 Skills | Skill anatomy, invocation model, authoring by hand and from a conversation |
| 04 MCP | MCP architecture, connecting remote endpoints, wrapping REST APIs as servers |
| 05 CLI | Headless -p runs, tool scoping, parallel isolation, and long-running goal patterns |
| 06 Sub-agents and Agent Teams | Sub-agent frontmatter, tool restriction, and orchestrator decomposition patterns; lead, teammates, shared task list and mailbox, and where a team beats a sub-agent |
| 07 Optimize and Observability | Prompt audit, caveman compression, the autoresearch promote/revert loop, prompt injection, hook guardrails, and tracing |
| 08 Distribution | Plugin manifests, converting a .claude/ config, and marketplace catalogs |
| 09 Agentic DevOps | Containerized deploy to a plain Ubuntu host over SSH, with the agent proving the result |
| 10 Agent SDK & Managed Agents | The query() async iterator, session resume, custom tools and typed output, then cloud containers, mid-execution steering via SSE, and multiagent with outcomes and memory |
| 11 Planning | Optional. Plan Mode mechanics, decomposition, and independently verifiable sub-agent packages |
hooks/intent-guard.mjs 279 lines1const PANE = "intent-guard";
2const SHELL_TOOLS = new Set(["Bash", "PowerShell"]);
3const PROMPT_WINDOW = 10;
4const MESSAGE_WINDOW = 3;
5const USER_ORIGINS = new Set(["composer", "bridge", "sdk"]);
6
7const promptsRef = { plugin: "intent-guard", key: "prompts" };
8const answerRef = { plugin: "intent-guard", key: "lastAnswer" };
9const heldRef = { plugin: "intent-guard", key: "held" };
10
11const SQL_TOOL = /(sqlcmd|Invoke-Sqlcmd|SqlConnection|SqlCommand|pyodbc|mssql-tools)/i;
12const SQL_WRITE = /\b(DELETE|DROP|TRUNCATE|UPDATE|INSERT|MERGE|ALTER|CREATE|RESTORE|BACKUP)\b/i;
13const INTERPRETERS = /^(python3?|py|node|bun|deno|bash|sh|zsh|pwsh|powershell|sqlcmd|psql|mysql|ssh|docker|kubectl|ruby|perl)$/i;
14const DB_SCRIPT = /^\s*(do\s+|then\s+)?(&\s*)?((bash|sh|pwsh|powershell|python3?|py)\s+(-\S+\s+)*)?["']?\S*?[\w-]*(push|restore|apply|sync|migrat|seed|override)[\w-]*\.(sh|ps1|py|cmd)\b|^\s*sqlpackage\b/i;
15const DB_WORD = /\b(db|sql|database|delta\w*)\b|-db\b|sqlpackage/i;
16
17const FAMILIES = [
18 {
19 id: "file-delete",
20 label: "deletes files",
21 match: (c) => segments(c).some((s) => /^\s*(sudo\s+)?(xargs\s+(-\S+\s+)*)?(rm|rmdir|del|erase|ri)\s|['"]\s*(rm|rmdir)\s|Remove-Item|\s-delete\b|git\s+rm\b|shutil\.rmtree|os\.remove\(|\.unlink\(/i.test(s) && !/(\\|\/)(temp|tmp)[\\\/._-]|scratchpad/i.test(s) && !createdHere(s, c)),
22 intent: /\b(delete|remove|rm|clean\s?up|wipe|purge|get rid|drop|do\s?n'?t need|no longer need|not needed|unused|l(ö|oe)sch\w*|entfern\w*)\b/i,
23 },
24 {
25 id: "git-discard",
26 label: "discards git work",
27 match: (c) => /git\s+(-C\s+\S+\s+)?(checkout\s+(--|\.)|restore\b|stash\b(?!\s+(list|show))|reset\s+--hard|clean\s+-\w*f|branch\s+-D)|git\s+(-C\s+\S+\s+)?push\s+.*(--force|\s-f\b)/i.test(c),
28 intent: /\b(revert|undo|discard|reset|restore|stash|throw away|roll\s?back|force|verwerf\w*|r(ü|ue)ckg(ä|ae)ngig|zur(ü|ue)cksetz\w*)\b/i,
29 },
30 {
31 id: "git-commit",
32 label: "commits",
33 match: (c) => /git\s+(-C\s+\S+\s+)?commit\b/i.test(c),
34 intent: /\b(commit\w*|check\s?in|einchecken)\b/i,
35 messageScoped: true,
36 },
37 {
38 id: "git-push",
39 label: "pushes",
40 match: (c) => /git\s+(-C\s+\S+\s+)?push\b/i.test(c),
41 intent: /\bpush\w*\b/i,
42 messageScoped: true,
43 },
44 {
45 id: "pr-merge",
46 label: "merges a pull request",
47 match: (c) => /gh\s+pr\s+merge\b/i.test(c),
48 intent: /\bmerge\w*\b/i,
49 messageScoped: true,
50 },
51 {
52 id: "db-write",
53 label: "writes to a database",
54 match: (c, tool, scripts) => (SQL_TOOL.test(c) && (SQL_WRITE.test(c) || SQL_WRITE.test(scripts) || scripts === null)) || (segments(c).some((s) => DB_SCRIPT.test(s)) && DB_WORD.test(c)),
55 intent: /\b(delete|update|insert|override|overwrite|restore|backup|drop|truncate|apply|migrate|seed|import|sync|copy|replace|write|change|modify|set|(ü|ue)berschreib\w*|wiederherstell\w*|einspiel\w*|l(ö|oe)sch\w*)\b/i,
56 },
57 {
58 id: "remote-destroy",
59 label: "deletes or stops remote resources",
60 match: (c) => /\b(hcloud|az|gh|doctl)\s+(\S+\s+){0,3}(?<!-)\b(delete|remove|destroy|purge)\b|docker\s+(compose\s+down|rm\b|rmi\b|volume\s+rm|system\s+prune|image\s+prune|container\s+prune|network\s+rm|stop\b|kill\b)/i.test(c),
61 intent: /\b(delete|remove|stop|tear\s?down|shut\s?down|decommission|destroy|clean\s?up|l(ö|oe)sch\w*|entfern\w*|stopp\w*|abschalt\w*)\b/i,
62 },
63 {
64 id: "dns",
65 label: "changes DNS",
66 match: (c) => /(hcloud\s+zone|az\s+network\s+dns|import-zonefile|rrset)/i.test(c) && !/\s(list|describe|show)\b/i.test(c),
67 intent: /\b(dns|zone|record\w*|delegation|nameserver\w*|cname|a-record)\b/i,
68 },
69 {
70 id: "process-kill",
71 label: "kills processes",
72 match: (c) => !/chrome-devtools-mcp/i.test(c) && /Stop-Process|taskkill|(^|\s)kill\s|\bpkill\b|\bkillall\b/i.test(c),
73 intent: /\b(kill|stop|restart|cycle|neu\s?start\w*|beend\w*)\b/i,
74 },
75 {
76 id: "test-run",
77 label: "runs tests",
78 match: (c) => segments(c).some((s) => /^\s*(do\s+|then\s+)?((npx|pnpm|yarn|bunx|bun)\s+)?(vitest|ng\s+test|npm\s+(run\s+)?test|playwright\s+test|dotnet\s+test|pytest|python3?\s+-m\s+pytest)\b/i.test(s)),
79 intent: /\b(test\w*|spec\w*|verify|verif\w*)\b/i,
80 messageScoped: true,
81 },
82 {
83 id: "mcp-delete",
84 label: "deletes through an MCP tool",
85 match: (c, tool) => /^mcp__.*(delete|remove|drop|purge|trash)/i.test(tool),
86 intent: /\b(delete|remove|drop|purge|trash|l(ö|oe)sch\w*|entfern\w*)\b/i,
87 },
88];
89
90const BLANKET = /\b(full permissions?|no checks?|volle (berechtigung|rechte))\b/i;
91const AFFIRMATIVE = /^\s*(y|yes|yep|ja|go|go ahead|do it|do that|ok|okay|sure|mach|mach es|passt|proceed|continue|weiter)\b/i;
92const GOAL = /(^|\s)\/goal\b/i;
93const NEGATION = /\b(don'?t|do not|never|no|not|nicht|kein\w*|nie)\b(\s+\S+){0,2}\s*$/i;
94
95
96export function register(on) {
97 on("session.start", async ($, e, next) => {
98 const result = await next(e);
99 await $.state.set(heldRef, []);
100 return result;
101 });
102
103 on("prompt.submit", async ($, e, next) => {
104 const result = await next(e);
105 if (!USER_ORIGINS.has(e.origin?.kind)) return result;
106 const text = String(e.text ?? "").trim();
107 if (text === "") return result;
108 const { value: answer = "" } = await $.state.get(answerRef);
109 const entry = AFFIRMATIVE.test(text) && answer ? `${text}\n${answer.slice(-2000)}` : text;
110 const { value: prompts = [] } = await $.state.get(promptsRef);
111 await $.state.set(promptsRef, [...prompts, entry].slice(-PROMPT_WINDOW));
112 return result;
113 });
114
115 on("turn.complete", async ($, e, next) => {
116 const result = await next(e);
117 if (!e.agentId && typeof e.answer === "string") await $.state.set(answerRef, e.answer);
118 return result;
119 });
120
121 on("tool.call", async ($, e, next) => {
122 const isShell = SHELL_TOOLS.has(e.tool);
123 if (!isShell && !String(e.tool).startsWith("mcp__")) return next(e);
124 const command = isShell ? expandVars(stripData(String(e.command ?? ""))) : `${e.tool} ${JSON.stringify(stripReserved(e))}`;
125 const scripts = isShell && SQL_TOOL.test(command) ? await readSqlScripts($, command) : "";
126 const risky = FAMILIES.filter((f) => (f.id === "mcp-delete" ? !isShell : isShell) && f.match(command, e.tool, scripts));
127 if (risky.length === 0) return next(e);
128
129 const { value: prompts = [] } = await $.state.get(promptsRef);
130 const unrequested = risky.filter((f) => !asked(f, prompts));
131 if (unrequested.length === 0) return next(e);
132
133 const cwd = await $.session.cwd().catch(() => "");
134
135 const item = {
136 id: e.tool_use_id ?? `${Date.now()}`,
137 tool: e.tool,
138 command: command.slice(0, 1500),
139 cwd,
140 agentId: e.agentId ?? null,
141 reasons: unrequested.map((f) => f.label),
142 lastPrompt: (prompts[prompts.length - 1] ?? "").split("\n")[0].slice(0, 300),
143 };
144 await pushHeld($, item).catch(() => {});
145 try {
146 await $.ui.open({ id: PANE, title: "Intent Guard" }).catch(() => {});
147 const why = `you did not ask for it (${item.reasons.join(", ")})`;
148 const who = item.agentId ? `Subagent ${item.agentId}` : "Claude";
149 const where = isShell && cwd ? ` in ${cwd}` : "";
150 const question = `${who} wants to run \`${brief(item.command, isShell, e.tool)}\`${where}, which ${item.reasons.join(" and ")}. Intent Guard held it because none of your recent prompts asked for that: run it?`;
151 const choice = await $.ui.ask(question, { header: "Guard", options: ["Proceed", "Cancel"] });
152 if (choice === "Proceed") return next(e);
153 return { deny: `Intent Guard: the user pressed ${JSON.stringify(choice)} instead of Proceed for this command. It was held because ${why}. Do not retry it or reach the same result another way; ask the user.` };
154 } catch {
155 return { deny: `Intent Guard held this command because the user did not ask for it (${item.reasons.join(", ")}), and nobody could confirm it. Do not retry it or reach the same result another way; ask the user.` };
156 } finally {
157 await dropHeld($, item.id).catch(() => {});
158 }
159 });
160
161 on("ui.render", { component: "Pane" }, async ($, e, next) => {
162 if (e.requestId !== PANE) return next(e);
163 const { value: held = [] } = await $.state.get(heldRef);
164 const { Box, Text } = $.ui.resolve(e);
165 if (held.length === 0) {
166 return Box({ flexDirection: "column", paddingX: 1, children: [Text({ dimColor: true, children: "Nothing held." })] });
167 }
168 const rows = held.flatMap((h, i) => [
169 Text({ bold: true, color: "yellow", children: `${i + 1}. ${h.tool}${h.agentId ? ` (agent ${h.agentId})` : ""}` }),
170 Text({ children: h.command }),
171 Text({ dimColor: true, children: `in ${h.cwd}` }),
172 Text({ color: "yellow", children: `No request found for: ${h.reasons.join(", ")}` }),
173 Text({ dimColor: true, children: `Your last prompt: ${h.lastPrompt || "(none)"}` }),
174 Text({ children: " " }),
175 ]);
176 return Box({ flexDirection: "column", paddingX: 1, children: rows });
177 });
178}
179
180function asked(family, prompts) {
181 const recent = prompts.slice(-(family.messageScoped ? MESSAGE_WINDOW : PROMPT_WINDOW));
182 const goals = family.messageScoped ? prompts.filter((p) => GOAL.test(p)) : [];
183 return [...recent, ...goals].some((p) => (!family.messageScoped && BLANKET.test(p)) || mentions(family.intent, p));
184}
185
186function stripData(command) {
187 const lines = command.split("\n");
188 const kept = [];
189 for (let i = 0; i < lines.length; i++) {
190 kept.push(lines[i]);
191 const m = lines[i].match(/<<-?\s*['"]?(\w+)['"]?/);
192 if (!m) continue;
193 const head = lines[i].slice(0, m.index).split(/&&|\|\||[;|(]/).pop().trim().split(/\s+/)[0] ?? "";
194 if (INTERPRETERS.test(head.replace(/^.*[\\/]/, ""))) continue;
195 while (i + 1 < lines.length && lines[i + 1].trim() !== m[1]) i++;
196 }
197 return kept.join("\n").replace(/(\s(-m|--message|--body|--title|--notes))\s+("(\\.|[^"\\])*"|'[^']*')/g, "$1 _");
198}
199
200function mentions(pattern, text) {
201 const source = pattern.source.replace(/\\b$/, "(e|ed|es|ing|s|d)?\\b");
202 const global = new RegExp(source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
203 for (const m of text.matchAll(global)) {
204 if (!NEGATION.test(text.slice(Math.max(0, m.index - 40), m.index))) return true;
205 }
206 return false;
207}
208
209function createdHere(segment, command) {
210 const targets = segment.replace(/^\s*(sudo\s+)?(rm|rmdir|del|erase)\s+/i, "").split(/\s+/).filter((t) => t && !t.startsWith("-"));
211 if (targets.length === 0 || !/^\s*(sudo\s+)?(rm|rmdir|del|erase)\s/i.test(segment)) return false;
212 const written = [...command.matchAll(/(>>?|\s-o|--output|Out-File|Set-Content)\s*["']?([^\s"'|;&]+)/gi)].map((m) => m[2]);
213 return targets.every((t) => written.includes(t.replace(/^["']|["']$/g, "")));
214}
215
216async function readSqlScripts($, command) {
217 const paths = [...segments(command).filter((s) => SQL_TOOL.test(s)).join(" ").matchAll(/\s-i\s+("([^"]+)"|'([^']+)'|(\S+))/g)].map((m) => m[2] ?? m[3] ?? m[4]);
218 if (paths.length === 0) return "";
219 try {
220 const texts = await Promise.all(paths.map((p) => $.fs.read(p.replace(/^\/([a-z])\//i, "$1:/"))));
221 return texts.join("\n");
222 } catch {
223 return null;
224 }
225}
226
227function segments(command) {
228 const out = [];
229 let current = "";
230 let quote = null;
231 for (let i = 0; i < command.length; i++) {
232 const ch = command[i];
233 if (quote) {
234 if (ch === quote && command[i - 1] !== "\\") quote = null;
235 current += ch;
236 } else if (ch === "'" || ch === '"') {
237 quote = ch;
238 current += ch;
239 } else if (ch === ";" || ch === "\n" || ch === "|" || (ch === "&" && command[i + 1] === "&")) {
240 out.push(current);
241 current = "";
242 if (command[i + 1] === ch) i++;
243 } else {
244 current += ch;
245 }
246 }
247 out.push(current);
248 return out;
249}
250
251function expandVars(command) {
252 const vars = {};
253 for (const m of command.matchAll(/(?:^|[\s;&])\$?([A-Za-z_]\w*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s;&]+))/g)) vars[m[1]] = m[3] ?? m[4] ?? m[5];
254 return command.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (all, name) => vars[name] ?? all);
255}
256
257
258function brief(command, isShell, tool) {
259 const text = isShell ? command.split("\n").find((l) => l.trim() !== "")?.trim() ?? "" : tool;
260 return text.length > 120 ? `${text.slice(0, 117)}...` : text;
261}
262
263function stripReserved(e) {
264 const { tool, tool_use_id, agentId, consent, ...rest } = e;
265 return rest;
266}
267
268async function pushHeld($, item) {
269 const { value: held = [] } = await $.state.get(heldRef);
270 await $.state.set(heldRef, [...held, item]);
271}
272
273async function dropHeld($, id) {
274 const { value: held = [] } = await $.state.get(heldRef);
275 const rest = held.filter((h) => h.id !== id);
276 await $.state.set(heldRef, rest);
277 if (rest.length === 0) await $.ui.close({ id: PANE });
278}
279types/index.d.ts 16 lines1export type IntentGuardHeld = {
2 id: string;
3 tool: string;
4 command: string;
5 cwd: string;
6 agentId: string | null;
7 reasons: string[];
8 lastPrompt: string;
9};
10
11declare module "claude-code" {
12 interface PluginState {
13 "intent-guard": { prompts: string[]; lastAnswer: string; held: IntentGuardHeld[] };
14 }
15}
16