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 286 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: "package-publish",
53 label: "publishes a package",
54 match: (c) => /\b(npm|pnpm|yarn)\s+publish\b|dotnet\s+nuget\s+push\b|\btwine\s+upload\b/i.test(c),
55 intent: /\b(publish\w*|release\w*|ship\w*)\b/i,
56 messageScoped: true,
57 },
58 {
59 id: "db-write",
60 label: "writes to a database",
61 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)),
62 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,
63 },
64 {
65 id: "remote-destroy",
66 label: "deletes or stops remote resources",
67 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),
68 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,
69 },
70 {
71 id: "dns",
72 label: "changes DNS",
73 match: (c) => /(hcloud\s+zone|az\s+network\s+dns|import-zonefile|rrset)/i.test(c) && !/\s(list|describe|show)\b/i.test(c),
74 intent: /\b(dns|zone|record\w*|delegation|nameserver\w*|cname|a-record)\b/i,
75 },
76 {
77 id: "process-kill",
78 label: "kills processes",
79 match: (c) => !/chrome-devtools-mcp/i.test(c) && /Stop-Process|taskkill|(^|\s)kill\s|\bpkill\b|\bkillall\b/i.test(c),
80 intent: /\b(kill|stop|restart|cycle|neu\s?start\w*|beend\w*)\b/i,
81 },
82 {
83 id: "test-run",
84 label: "runs tests",
85 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)),
86 intent: /\b(test\w*|spec\w*|verify|verif\w*)\b/i,
87 messageScoped: true,
88 },
89 {
90 id: "mcp-delete",
91 label: "deletes through an MCP tool",
92 match: (c, tool) => /^mcp__.*(delete|remove|drop|purge|trash)/i.test(tool),
93 intent: /\b(delete|remove|drop|purge|trash|l(ö|oe)sch\w*|entfern\w*)\b/i,
94 },
95];
96
97const BLANKET = /\b(full permissions?|no checks?|volle (berechtigung|rechte))\b/i;
98const 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;
99const GOAL = /(^|\s)\/goal\b/i;
100const NEGATION = /\b(don'?t|do not|never|no|not|nicht|kein\w*|nie)\b(\s+\S+){0,2}\s*$/i;
101
102
103export function register(on) {
104 on("session.start", async ($, e, next) => {
105 const result = await next(e);
106 await $.state.set(heldRef, []);
107 return result;
108 });
109
110 on("prompt.submit", async ($, e, next) => {
111 const result = await next(e);
112 if (!USER_ORIGINS.has(e.origin?.kind)) return result;
113 const text = String(e.text ?? "").trim();
114 if (text === "") return result;
115 const { value: answer = "" } = await $.state.get(answerRef);
116 const entry = AFFIRMATIVE.test(text) && answer ? `${text}\n${answer.slice(-2000)}` : text;
117 const { value: prompts = [] } = await $.state.get(promptsRef);
118 await $.state.set(promptsRef, [...prompts, entry].slice(-PROMPT_WINDOW));
119 return result;
120 });
121
122 on("turn.complete", async ($, e, next) => {
123 const result = await next(e);
124 if (!e.agentId && typeof e.answer === "string") await $.state.set(answerRef, e.answer);
125 return result;
126 });
127
128 on("tool.call", async ($, e, next) => {
129 const isShell = SHELL_TOOLS.has(e.tool);
130 if (!isShell && !String(e.tool).startsWith("mcp__")) return next(e);
131 const command = isShell ? expandVars(stripData(String(e.command ?? ""))) : `${e.tool} ${JSON.stringify(stripReserved(e))}`;
132 const scripts = isShell && SQL_TOOL.test(command) ? await readSqlScripts($, command) : "";
133 const risky = FAMILIES.filter((f) => (f.id === "mcp-delete" ? !isShell : isShell) && f.match(command, e.tool, scripts));
134 if (risky.length === 0) return next(e);
135
136 const { value: prompts = [] } = await $.state.get(promptsRef);
137 const unrequested = risky.filter((f) => !asked(f, prompts));
138 if (unrequested.length === 0) return next(e);
139
140 const cwd = await $.session.cwd().catch(() => "");
141
142 const item = {
143 id: e.tool_use_id ?? `${Date.now()}`,
144 tool: e.tool,
145 command: command.slice(0, 1500),
146 cwd,
147 agentId: e.agentId ?? null,
148 reasons: unrequested.map((f) => f.label),
149 lastPrompt: (prompts[prompts.length - 1] ?? "").split("\n")[0].slice(0, 300),
150 };
151 await pushHeld($, item).catch(() => {});
152 try {
153 await $.ui.open({ id: PANE, title: "Intent Guard" }).catch(() => {});
154 const why = `you did not ask for it (${item.reasons.join(", ")})`;
155 const who = item.agentId ? `Subagent ${item.agentId}` : "Claude";
156 const where = isShell && cwd ? ` in ${cwd}` : "";
157 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?`;
158 const choice = await $.ui.ask(question, { header: "Guard", options: ["Proceed", "Cancel"] });
159 if (choice === "Proceed") return next(e);
160 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.` };
161 } catch {
162 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.` };
163 } finally {
164 await dropHeld($, item.id).catch(() => {});
165 }
166 });
167
168 on("ui.render", { component: "Pane" }, async ($, e, next) => {
169 if (e.requestId !== PANE) return next(e);
170 const { value: held = [] } = await $.state.get(heldRef);
171 const { Box, Text } = $.ui.resolve(e);
172 if (held.length === 0) {
173 return Box({ flexDirection: "column", paddingX: 1, children: [Text({ dimColor: true, children: "Nothing held." })] });
174 }
175 const rows = held.flatMap((h, i) => [
176 Text({ bold: true, color: "yellow", children: `${i + 1}. ${h.tool}${h.agentId ? ` (agent ${h.agentId})` : ""}` }),
177 Text({ children: h.command }),
178 Text({ dimColor: true, children: `in ${h.cwd}` }),
179 Text({ color: "yellow", children: `No request found for: ${h.reasons.join(", ")}` }),
180 Text({ dimColor: true, children: `Your last prompt: ${h.lastPrompt || "(none)"}` }),
181 Text({ children: " " }),
182 ]);
183 return Box({ flexDirection: "column", paddingX: 1, children: rows });
184 });
185}
186
187function asked(family, prompts) {
188 const recent = prompts.slice(-(family.messageScoped ? MESSAGE_WINDOW : PROMPT_WINDOW));
189 const goals = family.messageScoped ? prompts.filter((p) => GOAL.test(p)) : [];
190 return [...recent, ...goals].some((p) => (!family.messageScoped && BLANKET.test(p)) || mentions(family.intent, p));
191}
192
193function stripData(command) {
194 const lines = command.split("\n");
195 const kept = [];
196 for (let i = 0; i < lines.length; i++) {
197 kept.push(lines[i]);
198 const m = lines[i].match(/<<-?\s*['"]?(\w+)['"]?/);
199 if (!m) continue;
200 const head = lines[i].slice(0, m.index).split(/&&|\|\||[;|(]/).pop().trim().split(/\s+/)[0] ?? "";
201 if (INTERPRETERS.test(head.replace(/^.*[\\/]/, ""))) continue;
202 while (i + 1 < lines.length && lines[i + 1].trim() !== m[1]) i++;
203 }
204 return kept.join("\n").replace(/(\s(-m|--message|--body|--title|--notes))\s+("(\\.|[^"\\])*"|'[^']*')/g, "$1 _");
205}
206
207function mentions(pattern, text) {
208 const source = pattern.source.replace(/\\b$/, "(e|ed|es|ing|s|d)?\\b");
209 const global = new RegExp(source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
210 for (const m of text.matchAll(global)) {
211 if (!NEGATION.test(text.slice(Math.max(0, m.index - 40), m.index))) return true;
212 }
213 return false;
214}
215
216function createdHere(segment, command) {
217 const targets = segment.replace(/^\s*(sudo\s+)?(rm|rmdir|del|erase)\s+/i, "").split(/\s+/).filter((t) => t && !t.startsWith("-"));
218 if (targets.length === 0 || !/^\s*(sudo\s+)?(rm|rmdir|del|erase)\s/i.test(segment)) return false;
219 const written = [...command.matchAll(/(>>?|\s-o|--output|Out-File|Set-Content)\s*["']?([^\s"'|;&]+)/gi)].map((m) => m[2]);
220 return targets.every((t) => written.includes(t.replace(/^["']|["']$/g, "")));
221}
222
223async function readSqlScripts($, command) {
224 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]);
225 if (paths.length === 0) return "";
226 try {
227 const texts = await Promise.all(paths.map((p) => $.fs.read(p.replace(/^\/([a-z])\//i, "$1:/"))));
228 return texts.join("\n");
229 } catch {
230 return null;
231 }
232}
233
234function segments(command) {
235 const out = [];
236 let current = "";
237 let quote = null;
238 for (let i = 0; i < command.length; i++) {
239 const ch = command[i];
240 if (quote) {
241 if (ch === quote && command[i - 1] !== "\\") quote = null;
242 current += ch;
243 } else if (ch === "'" || ch === '"') {
244 quote = ch;
245 current += ch;
246 } else if (ch === ";" || ch === "\n" || ch === "|" || (ch === "&" && command[i + 1] === "&")) {
247 out.push(current);
248 current = "";
249 if (command[i + 1] === ch) i++;
250 } else {
251 current += ch;
252 }
253 }
254 out.push(current);
255 return out;
256}
257
258function expandVars(command) {
259 const vars = {};
260 for (const m of command.matchAll(/(?:^|[\s;&])\$?([A-Za-z_]\w*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s;&]+))/g)) vars[m[1]] = m[3] ?? m[4] ?? m[5];
261 return command.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (all, name) => vars[name] ?? all);
262}
263
264
265function brief(command, isShell, tool) {
266 const text = isShell ? command.split("\n").find((l) => l.trim() !== "")?.trim() ?? "" : tool;
267 return text.length > 120 ? `${text.slice(0, 117)}...` : text;
268}
269
270function stripReserved(e) {
271 const { tool, tool_use_id, agentId, consent, ...rest } = e;
272 return rest;
273}
274
275async function pushHeld($, item) {
276 const { value: held = [] } = await $.state.get(heldRef);
277 await $.state.set(heldRef, [...held, item]);
278}
279
280async function dropHeld($, id) {
281 const { value: held = [] } = await $.state.get(heldRef);
282 const rest = held.filter((h) => h.id !== id);
283 await $.state.set(heldRef, rest);
284 if (rest.length === 0) await $.ui.close({ id: PANE });
285}
286types/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