When an answer ends in a question, shows the user's own earlier sentence that already answers it and sends it back on one key

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/ask-check.mjs 173 lines1const PROMPT_WINDOW = 40;
2const USER_ORIGINS = new Set(["composer", "bridge", "sdk"]);
3
4const promptsRef = { plugin: "ask-check", key: "prompts" };
5const questionsRef = { plugin: "ask-check", key: "questions" };
6const countRef = { plugin: "ask-check", key: "count" };
7
8const ASKED_WINDOW = 40;
9const NO_QUESTIONS = /\b(no (other |more |further )?(questions?|q)\b|(do ?n.?t|stop|never) (ask|bother|bloat)\w*( me)?( with)? (any |more )?(questions?|findings)|without questions|bloat\w* me with questions|(invent|make up) (findings and )?questions)/i;
10const askedRef = { plugin: "ask-check", key: "asked" };
11const answersRef = { plugin: "ask-check", key: "answers" };
12const ANSWER_WINDOW = 10;
13const LEAD = { rule: "you said", said: "you said", replied: "asked before, you replied" };
14
15const STOP = new Set(`
16a about above after again all also am an and any are as at be because been before being below between both but by can could did do does doing done down during each either else even ever every few for from further get got had has have having he her here hers him his how i if in into is it its just keep let like make may me might mine more most much must my no nor not now of off on once one only or other our out over own please put rather really same see shall she should since so some still such sure than that the their them then there these they this those through to too under until up upon us use used very via want was way we were what when where whether which while who whom why will with would yes yet you your yours
17able already anything currently either exactly instead maybe need needs next okay ok prefer right something thing things think whatever wish wanted proceed option options go ahead fine
18aber alle als also am an auch auf aus bei bin bis bitte da dann das dass dem den der des die dies diese doch du ein eine einem einen einer es für hat hier ich ihr im in ist ja jetzt kann mit nach nicht noch nur oder schon sein sich sie sind so und uns von vor was wenn wie wir wird zu zum zur
19`.split(/\s+/).filter(Boolean));
20
21export function register(on) {
22 on("prompt.submit", async ($, e, next) => {
23 const result = await next(e);
24 if (!USER_ORIGINS.has(e.origin?.kind)) return result;
25 const text = String(e.text ?? "").trim();
26 if (text === "") return result;
27 const { value: count = 0 } = await $.state.get(countRef);
28 const { value: prompts = [] } = await $.state.get(promptsRef);
29 await $.state.set(countRef, count + 1);
30 await $.state.set(promptsRef, [...prompts, { n: count + 1, text: text.slice(0, 4000) }].slice(-PROMPT_WINDOW));
31 await $.state.set(questionsRef, []);
32 return result;
33 });
34
35 on("turn.complete", async ($, e, next) => {
36 const result = await next(e);
37 if (e.agentId || e.reason !== "answer" || typeof e.answer !== "string") return result;
38 const { value: prompts = [] } = await $.state.get(promptsRef);
39 const { value: asked = [] } = await $.state.get(askedRef);
40 const { value: count = 0 } = await $.state.get(countRef);
41 const { value: answers = [] } = await $.state.get(answersRef);
42 const questions = checkAnswer(e.answer, prompts, asked, answers);
43 await $.state.set(answersRef, [...answers, normalize(e.answer).slice(0, 8000)].slice(-ANSWER_WINDOW));
44 await $.state.set(questionsRef, questions);
45 if (questions.length) await $.state.set(askedRef, [...asked, ...questions.map((q) => ({ question: q.question, n: count }))].slice(-ASKED_WINDOW));
46 return result;
47 });
48
49 on("ui.render", { component: "AbovePrompt" }, async ($, e, next) => {
50 const below = await next(e);
51 const { value: questions = [] } = await $.state.get(questionsRef);
52 if (e.props.hasSurvey || e.props.isWorking || questions.length === 0) return below;
53 const answered = questions.filter((q) => q.answer && q.answer.via !== "replied");
54 const before = questions.filter((q) => q.answer?.via === "replied");
55 const matched = [...answered, ...before];
56 const { Box, Text, Button } = $.ui.resolve(e);
57 const width = Math.max(20, (e.props.bodyColumns ?? 80) - 4);
58 const summary = [`asked ${questions.length}`, `already answered ${answered.length}`, ...(before.length ? [`asked before ${before.length}`] : [])].join(" · ");
59 const rows = [
60 Box({
61 flexDirection: "row",
62 children: [
63 Text({ color: matched.length ? "yellow" : undefined, dimColor: matched.length === 0, children: summary }),
64 ...(matched.length
65 ? [Text({ children: " " }), Button({ key: "send", label: "send already answered", hotkey: "1", plain: true, onPress: () => sendAnswered($) })]
66 : []),
67 ],
68 }),
69 ...matched.map((q) =>
70 Text({ wrap: "truncate-end", children: ` ↳ ${LEAD[q.answer.via]} "${clip(q.answer.text, width - 34)}" (prompt ${q.answer.n})` }),
71 ),
72 ];
73 const mine = Box({ flexDirection: "column", paddingX: 1, children: rows });
74 return below ? Box({ flexDirection: "column", children: [mine, below] }) : mine;
75 });
76}
77
78async function sendAnswered($) {
79 const { value: questions = [] } = await $.state.get(questionsRef);
80 const matched = questions.filter((q) => q.answer);
81 if (matched.length === 0) return;
82 const unique = [...new Map(matched.map((q) => [q.answer.text, q.answer])).values()];
83 const said = unique.filter((a) => a.via !== "replied").map((a) => `"${a.text}" (my prompt ${a.n})`);
84 const replied = unique.filter((a) => a.via === "replied").map((a) => `"${a.text}" (my prompt ${a.n})`);
85 const parts = [...(said.length ? [`Already answered: ${said.join("; ")}.`] : []), ...(replied.length ? [`You asked this before and I replied: ${replied.join("; ")}.`] : [])];
86 await $.state.set(questionsRef, []);
87 await $.prompt.submit({ text: `${parts.join(" ")} Proceed.`, asUser: true });
88}
89
90export function checkAnswer(answer, prompts, asked = [], answers = []) {
91 const standing = standingRule(prompts);
92 return questionsIn(answer).map((question) => ({
93 question,
94 answer: standing ?? repliedBefore(question, prompts, asked) ?? findAnswer(question, prompts, answers),
95 }));
96}
97
98function standingRule(prompts) {
99 for (let i = prompts.length - 1; i >= 0; i--) {
100 const hit = clauses(prompts[i].text).find((c) => NO_QUESTIONS.test(c));
101 if (hit) return { text: hit, n: prompts[i].n, via: "rule" };
102 }
103 return null;
104}
105
106function repliedBefore(question, prompts, asked) {
107 const q = new Set(tokens(question));
108 if (q.size === 0) return null;
109 for (let i = asked.length - 1; i >= 0; i--) {
110 const before = new Set(tokens(asked[i].question));
111 let shared = 0;
112 for (const t of before) if (q.has(t)) shared++;
113 if (shared < 2 || shared / Math.min(q.size, before.size) < 0.5) continue;
114 const reply = prompts.find((p) => p.n === asked[i].n + 1);
115 if (reply) return { text: clip(reply.text.replace(/\s+/g, " "), 300), n: reply.n, via: "replied" };
116 }
117 return null;
118}
119
120export function questionsIn(answer) {
121 const prose = answer.replace(/```[\s\S]*?```/g, " ").replace(/`[^`\n]*`/g, (m) => m.slice(1, -1));
122 return prose
123 .split(/\n+|(?<=[.!?])\s+(?=[A-Z0-9"*(])/)
124 .map((s) => s.replace(/^[\s>*#\-\d.)]+/, "").replace(/\*\*/g, "").trim())
125 .filter((s) => s.endsWith("?") && s.length >= 12 && s.length <= 500);
126}
127
128export function findAnswer(question, prompts, answers = []) {
129 const q = new Set(tokens(question));
130 if (q.size === 0) return null;
131 let best = null;
132 for (const p of prompts) {
133 for (const clause of clauses(p.text)) {
134 if (clause.length >= 25 && answers.some((a) => a.includes(normalize(clause)))) continue;
135 const c = new Set(tokens(clause));
136 let shared = 0;
137 for (const t of c) if (q.has(t)) shared++;
138 const ratio = shared / q.size;
139 if (shared < 2 || ratio < 0.45) continue;
140 const score = shared + ratio;
141 if (!best || score >= best.score) best = { score, text: clause, n: p.n };
142 }
143 }
144 return best ? { text: best.text, n: best.n, via: "said" } : null;
145}
146
147function clauses(text) {
148 return text
149 .replace(/<pasted_content[\s\S]*?<\/pasted_content[^>]*>/g, " ")
150 .split(/\n+|\s*(?:\.\.\.|…|->|[.!?;])\s+|\s+\d+[.):]\s+/)
151 .map((s) => s.trim())
152 .filter((s) => s.length >= 3 && s.length <= 300);
153}
154
155function tokens(text) {
156 return (text.toLowerCase().match(/[a-zäöüß0-9][a-zäöüß0-9._-]*[a-zäöüß0-9]/g) ?? [])
157 .filter((w) => w.length >= 3 && !STOP.has(w) && !/^\d+$/.test(w))
158 .map(stem);
159}
160
161function stem(w) {
162 if (/[._-]/.test(w)) return w;
163 return w.replace(/(ing|ed|es|s)$/, "").replace(/(.)\1$/, "$1");
164}
165
166function normalize(text) {
167 return text.toLowerCase().replace(/[`*_]/g, "").replace(/\s+/g, " ");
168}
169
170function clip(text, max) {
171 return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text;
172}
173types/index.d.ts 10 lines1export type AskCheckPrompt = { n: number; text: string };
2export type AskCheckQuestion = { question: string; answer: { text: string; n: number } | null };
3export type AskCheckAsked = { question: string; n: number };
4
5declare module "claude-code" {
6 interface PluginState {
7 "ask-check": { prompts: AskCheckPrompt[]; questions: AskCheckQuestion[]; asked: AskCheckAsked[]; answers: string[]; count: number };
8 }
9}
10