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 286 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: "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}
286
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