SLOPSHOPPER

intent-guard

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

newpaneguardprompt
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · intent-guard
│ ┃ intent-guard ✕ › fix the failing auth test and add an audit log call │ ┃ Nothing held. │ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(rm -rf build && git push --force origin main) │ ⎿ Denied by intent-guard: Intent Guard: the user pressed "C │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · intent-guard
Nothing held.
README

Masterclass: Agentic Software Engineering with Claude Code

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.

ModuleDescription
01 FundamentalsClaude product family, installation and authentication, model selection and prompt caching
02 HarnessSettings hierarchy, CLAUDE.md constitution, custom slash commands, and event-driven hooks
03 SkillsSkill anatomy, invocation model, authoring by hand and from a conversation
04 MCPMCP architecture, connecting remote endpoints, wrapping REST APIs as servers
05 CLIHeadless -p runs, tool scoping, parallel isolation, and long-running goal patterns
06 Sub-agents and Agent TeamsSub-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 ObservabilityPrompt audit, caveman compression, the autoresearch promote/revert loop, prompt injection, hook guardrails, and tracing
08 DistributionPlugin manifests, converting a .claude/ config, and marketplace catalogs
09 Agentic DevOpsContainerized deploy to a plain Ubuntu host over SSH, with the agent proving the result
10 Agent SDK & Managed AgentsThe 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 PlanningOptional. Plan Mode mechanics, decomposition, and independently verifiable sub-agent packages
Source 2 files
hooks/intent-guard.mjs 279 lines
1const 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}
279
types/index.d.ts 16 lines
1export 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