<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Homingo Learn Guides]]></title><description><![CDATA[Homingo Learn Guides]]></description><link>https://homingolearn.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ae8678367ff2256c81c0098/51a08a9b-7bd7-4408-9db3-8cc9f1e1aa72.png</url><title>Homingo Learn Guides</title><link>https://homingolearn.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 03 Nov 2026 02:35:05 GMT</lastBuildDate><atom:link href="https://homingolearn.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Claude Code custom slash commands: how to create them (with examples)]]></title><description><![CDATA[How to create custom slash commands in Claude Code: file locations, frontmatter, $ARGUMENTS and named arguments, shell output, and commands vs skills.]]></description><link>https://homingolearn.hashnode.dev/claude-code-custom-slash-commands-how-to-create-them-with-examples</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/claude-code-custom-slash-commands-how-to-create-them-with-examples</guid><category><![CDATA[claude-code]]></category><category><![CDATA[AI]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 18:50:26 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://learn.homingo.co.in/guides/claude-code-custom-slash-commands"><em>Homingo Learn</em></a><em>.</em></p>
<p><em>Custom slash commands in Claude Code turn a prompt you type every day into a one-word command, with arguments and live project context filled in before Claude reads it.</em></p>
<h2>Custom commands and skills: the same thing now</h2>
<p>Custom slash commands have been merged into skills. A file at <code>.claude/commands/review.md</code> and a skill at <code>.claude/skills/review/SKILL.md</code> both create <code>/review</code> and behave the same way; existing command files keep working. Claude can also run either one by itself when its description matches the task, unless you set <code>disable-model-invocation: true</code>. The single-file command is the quickest to write. Choose the skill folder when the command needs reference files or scripts next to it (see <a href="https://learn.homingo.co.in/guides/how-to-write-skill-md-claude-code">how to write a SKILL.md</a>). If both exist with the same name, the skill wins.</p>
<h2>How to create a custom slash command (.claude/commands)</h2>
<p>The file name becomes the command name. Subfolders become a namespace: <code>.claude/commands/video/qa.md</code> is <code>/video:qa</code>. Commit <code>.claude/commands/</code> to share commands with the team; personal ones go in your home folder as skills (<code>~/.claude/skills/&lt;name&gt;/SKILL.md</code>).</p>
<p><em>.claude/commands/explain-error.md</em></p>
<pre><code class="language-markdown">---
description: Explain an error message and propose the smallest fix
argument-hint: "[error text]"
---

Explain this error in two sentences, find where it comes from in this repository,
and propose the smallest change that fixes it. Do not edit files until I agree.

Error: $ARGUMENTS
</code></pre>
<p>Type <code>/explain-error TypeError: cannot read properties of undefined</code> and everything after the command name replaces <code>$ARGUMENTS</code>. If a command has no placeholder, Claude Code appends the arguments at the end as <code>ARGUMENTS: ...</code>.</p>
<h2>Arguments: $ARGUMENTS, $0 and named arguments</h2>
<ul>
<li><code>$ARGUMENTS</code>: everything typed after the command.</li>
<li><code>$0</code>, <code>$1</code> … (or <code>$ARGUMENTS[0]</code>): single arguments, counted from zero (older guides start at <code>$1</code>, so check old command files). Quote an argument that has spaces: <code>/new-episode ep05 "a lighthouse keeper hears knocking"</code> gives <code>$0</code> = <code>ep05</code> and <code>$1</code> = the whole premise.</li>
<li><strong>Named arguments:</strong> list them in the frontmatter (<code>arguments: [id, premise]</code>) and write <code>$id</code> and <code>$premise</code> in the body. Easier to read than positions.</li>
<li>A literal dollar sign needs a backslash: <code>\$1.00</code>.</li>
</ul>
<p><em>.claude/commands/new-episode.md</em></p>
<pre><code class="language-markdown">---
description: Start a new episode spec from the template
argument-hint: "[episode-id] [premise]"
arguments: [id, premise]
---

Create episodes/$id/spec.json from episodes/_template/spec.json.

Premise: $premise

1. Write the hook shot first: frame 0 must show a situation that raises a question.
2. Fill the remaining shots so each one adds information or raises a new question.
3. Write one caption per shot, at most 7 words.
4. Run `npm test` and fix every spec error before you stop.
</code></pre>
<h2>Run shell commands in a slash command (! syntax)</h2>
<p>Write <code>!</code> followed by a command in backticks, at the start of a line or after a space: the command runs <em>before</em> the prompt is sent, and its output replaces the placeholder. For several commands, use a fenced code block whose opening fence is followed by <code>!</code>. Claude reads the current state instead of spending turns finding it. These commands are checked against your permission rules and do not prompt, so pre-approve them with <code>allowed-tools</code>; if one fails, the whole command is aborted.</p>
<p><em>.claude/commands/review-changes.md</em></p>
<pre><code class="language-markdown">---
description: Review my uncommitted changes against the project rules
allowed-tools: Bash(git status *) Bash(git diff *)
---

## Changed files
!`git status --short`

## Diff
!`git diff HEAD`

Review the diff above against CLAUDE.md. List every problem as
file:line, what is wrong, and the fix. Nothing else.
</code></pre>
<p>New untracked files show in <code>git status</code> but not in the diff; run <code>git add -N</code> on them first if the review should read them. In a repository with no commits yet, use <code>git diff</code> instead of <code>git diff HEAD</code>.</p>
<h2>Frontmatter fields worth knowing</h2>
<ul>
<li><code>description</code>: shown in the <code>/</code> menu. Without it, the first line of the body is used.</li>
<li><code>argument-hint</code>: the hint shown while you type, such as <code>[episode-id]</code>.</li>
<li><code>allowed-tools</code>: tools Claude may use without asking during this command, for example <code>Bash(ffprobe *)</code>. Deny rules still win.</li>
<li><code>model</code> and <code>effort</code>: run a routine command on a smaller model or lower effort.</li>
<li><code>disable-model-invocation: true</code>: only you can trigger it. Use it for anything with side effects, such as a deploy or a publish.</li>
<li><code>user-invocable: false</code>: the opposite; hidden from the <code>/</code> menu, for background knowledge only Claude should load.</li>
<li><code>context: fork</code> (with <code>agent</code> to pick the type): run the command in a subagent with its own context.</li>
</ul>
<p>Field names must match exactly: Claude Code ignores an unknown field without an error, so a typo silently does nothing.</p>
<p><em>.claude/commands/video/qa.md (run as /video:qa ep05)</em></p>
<pre><code class="language-markdown">---
description: Technical QA of a rendered episode before posting
argument-hint: "[episode-id]"
allowed-tools: Bash(ffprobe *)
model: haiku
disable-model-invocation: true
---

Run ffprobe on out/$0.mp4 and report width, height, frame rate, duration and
whether an audio stream exists. PASS only if it is 1080x1920, r_frame_rate is exactly
30/1, and it is between 15 and 90 seconds long. One line per check, then PASS or FAIL.
</code></pre>
<h2>Commands, skills, subagents or hooks?</h2>
<ul>
<li><strong>A command</strong> when you start the job yourself and the prompt is the same each time.</li>
<li><strong>A skill</strong> when the method is longer or needs files beside it.</li>
<li><strong>A</strong> <a href="https://learn.homingo.co.in/guides/claude-code-subagents-guide"><strong>subagent</strong></a> when the work would fill your main conversation with file reads, or needs a reviewer that did not write it. Define one first (a <code>file-qa</code> agent, say), then the command can simply say “use the file-qa subagent on out/$0.mp4”, or set <code>context: fork</code> to run the command itself in a subagent.</li>
<li><strong>A</strong> <a href="https://learn.homingo.co.in/guides/claude-code-hooks-examples"><strong>hook</strong></a> when something must happen every time, with no prompt at all.</li>
</ul>
<h2>Command not showing up?</h2>
<ul>
<li>Type <code>/</code> and look for the name: the menu lists every command, and every skill not marked <code>user-invocable: false</code>.</li>
<li>Check the folder: <code>.claude/commands/</code> in the project you started Claude in (or a skill folder with a <code>SKILL.md</code> inside).</li>
<li>Check for a clash: a skill with the same name wins over a command file.</li>
<li>Check the frontmatter: it must open and close with <code>---</code> and be valid YAML.</li>
</ul>
<h2>Common mistakes</h2>
<ul>
<li><strong>A shell line missing from</strong> <code>allowed-tools</code><strong>.</strong> Add the exact pattern.</li>
<li><strong>Unquoted multi-word arguments.</strong> Without quotes, each word is its own <code>$N</code>.</li>
<li><strong>Side effects Claude can trigger.</strong> Add <code>disable-model-invocation: true</code> to deploys and publishes.</li>
<li><strong>Rules copied into every command.</strong> Shared rules belong in <a href="https://learn.homingo.co.in/guides/claude-md-best-practices">CLAUDE.md</a>; the command keeps only its own task.</li>
</ul>
<p>If you make short vertical videos with Claude Code, the skill below packages a complete spec-first workflow like the one behind the episode commands above.</p>
<blockquote>
<p>Fields and syntax follow the <a href="https://code.claude.com/docs/en/slash-commands">official skills and commands documentation</a> as of November 2026.</p>
</blockquote>
<blockquote>
<p><strong>Animated horror reels skill</strong>: Our whole playbook as a Claude Code agent skill: the hook-first story method, the episode spec template, the camera, light and grade system with a palette builder, a render-ready Remotion starter and the QA checks before you post. Make reels like ours. <a href="https://learn.homingo.co.in/skill?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=claude-code-custom-slash-commands">See the skill</a></p>
</blockquote>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the skill linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[Isometric diagrams in plain SVG: projection, four-tone shading and painter's order]]></title><description><![CDATA[Draw isometric diagrams in plain SVG with about 45 lines of JavaScript: the projection, four-tone shading from one colour, and a painter's order that…]]></description><link>https://homingolearn.hashnode.dev/isometric-diagrams-in-plain-svg-projection-four-tone-shading-and-painter-s-order</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/isometric-diagrams-in-plain-svg-projection-four-tone-shading-and-painter-s-order</guid><category><![CDATA[SVG]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 17:06:31 GMT</pubDate><content:encoded><![CDATA[<p>Isometric drawings explain hardware and systems better than flat boxes and arrows: a reader sees depth, which part sits on which, and where things connect. You don't need a 3D engine for them. About 45 lines of JavaScript and plain SVG polygons are enough to start, and the three ideas below are the ones that decide whether the result looks crafted or broken.</p>
<p>The script at the end draws a small machine: a base plate with a tank, a pipe, a pump and a valve.</p>
<h2>1. The projection</h2>
<p>In a classic isometric view the x and y axes sit 30 degrees below the horizontal and z points straight up. A world point (x, y, z) lands on screen at:</p>
<pre><code class="language-js">const COS30 = Math.cos(Math.PI / 6), SIN30 = 0.5;

function project([x, y, z], s = 20, ox = 280, oy = 190) {
  return [ox + (x - y) * COS30 * s, oy + (x + y) * SIN30 * s - z * s];
}
</code></pre>
<p><code>s</code> is the pixel size of one world unit and <code>(ox, oy)</code> is where the world origin lands. On screen +x runs down-right, +y runs down-left and +z runs up. That means a bigger <code>x + y</code> is closer to the viewer, which matters in step 3.</p>
<h2>2. A box is three faces, and each face gets its own tone</h2>
<p>From this camera you only ever see three faces of a box: the top, the face at <code>x + w</code> (screen right) and the face at <code>y + d</code> (screen left). Draw those three as polygons and skip the rest.</p>
<p>Flat colour makes boxes look like stickers. The trick is four tones from one base colour: the top lightest (it faces the light), the right face at the base colour, the left face darker, and an outline darker still so edges read on any background.</p>
<pre><code class="language-js">function tones(hex) {
  const n = parseInt(hex.slice(1), 16), c = [n &gt;&gt; 16, (n &gt;&gt; 8) &amp; 255, n &amp; 255];
  const mix = k =&gt; '#' + c.map(v =&gt; Math.round(Math.min(255, v * k)).toString(16).padStart(2, '0')).join('');
  return { top: mix(1.25), right: mix(1.0), left: mix(0.75), line: mix(0.45) };
}
</code></pre>
<p>Keep the same four multipliers for every material and the whole drawing reads as lit from one direction.</p>
<h2>3. Painter's order (the bug that bites first)</h2>
<p>SVG paints in document order, so whatever comes later covers what came before. You have to emit far things first.</p>
<p>The obvious first try is to sort every box by its near corner, <code>(x + w) + (y + d)</code>. It looks reasonable on paper and fails at once on this scene: the base plate is the widest part, so its near corner is the biggest, so it is drawn last and buries the pipe and the valve, the lower half of the tank and most of the pump.</p>
<p>For drawings built in layers (a plate, then parts standing on it) a two-key sort fixes it: the bottom <code>z</code> first, then the near corner inside each layer.</p>
<pre><code class="language-js">const order = (a, b) =&gt; (a.z - b.z) || ((a.x + a.w + a.y + a.d) - (b.x + b.w + b.y + b.d));
</code></pre>
<p>Know its limits. It holds for compact, similar-sized parts. A long member breaks it even when nothing touches: a long pipe running along x behind a short pump has the bigger near-corner sum, so it gets drawn after the pump and paints over it. Parts that float above another layer can fail the same way. For anything beyond simple layered scenes, use the separation test: with this camera, box A is behind box B when <code>A.x + A.w &lt;= B.x</code>, or <code>A.y + A.d &lt;= B.y</code>, or <code>A.z + A.h &lt;= B.z</code>. Add an edge for each pair whose screen outlines overlap, then sort topologically. If the graph has a cycle (three bars in a pinwheel, each in front of the next), split one box in two to break it.</p>
<h2>The whole script</h2>
<p>Save it as <code>iso.mjs</code> and run <code>node iso.mjs &gt; machine.svg</code>. Open the SVG in any browser. Node 18 or newer is enough; there are no dependencies.</p>
<pre><code class="language-js">// iso.mjs: isometric boxes in plain SVG, no libraries. Run: node iso.mjs &gt; machine.svg
const COS30 = Math.cos(Math.PI / 6), SIN30 = 0.5;

// World -&gt; screen. On screen, +x runs down-right, +y runs down-left, +z runs straight up. One unit = `s` px.
function project([x, y, z], s = 20, ox = 280, oy = 190) {
  return [ox + (x - y) * COS30 * s, oy + (x + y) * SIN30 * s - z * s];
}

// Four tones per material: top lightest, right face mid, left face darker, outline darkest.
function tones(hex) {
  const n = parseInt(hex.slice(1), 16), c = [n &gt;&gt; 16, (n &gt;&gt; 8) &amp; 255, n &amp; 255];
  const mix = k =&gt; '#' + c.map(v =&gt; Math.round(Math.min(255, v * k)).toString(16).padStart(2, '0')).join('');
  return { top: mix(1.25), right: mix(1.0), left: mix(0.75), line: mix(0.45) };
}

const pts = p =&gt; p.map(q =&gt; project(q).map(v =&gt; v.toFixed(1)).join(',')).join(' ');

// A box at (x, y, z) with size (w, d, h). Only the three faces the viewer sees are drawn.
function box({ x, y, z, w, d, h, color }) {
  const t = tones(color);
  const top   = [[x, y, z + h], [x + w, y, z + h], [x + w, y + d, z + h], [x, y + d, z + h]];
  const right = [[x + w, y, z], [x + w, y + d, z], [x + w, y + d, z + h], [x + w, y, z + h]];
  const left  = [[x, y + d, z], [x + w, y + d, z], [x + w, y + d, z + h], [x, y + d, z + h]];
  const face = (p, fill) =&gt; `&lt;polygon points="${pts(p)}" fill="${fill}" stroke="${t.line}" stroke-width="1" stroke-linejoin="round"/&gt;`;
  return face(left, t.left) + face(right, t.right) + face(top, t.top);
}

// Painter's order: draw what is farther from the viewer first. For parts that sit on layers
// (a plate, then things standing on it), sort by the bottom z first, then by the near corner
// (x + w) + (y + d) inside each layer.
const order = (a, b) =&gt; (a.z - b.z) || ((a.x + a.w + a.y + a.d) - (b.x + b.w + b.y + b.d));

const parts = [
  { x: 0, y: 0, z: 0,   w: 10, d: 6, h: 1,   color: '#5b6b82' }, // base plate
  { x: 1, y: 1, z: 1,   w: 3,  d: 3, h: 5,   color: '#2f7de1' }, // tank
  { x: 4, y: 2, z: 1,   w: 3,  d: 1, h: 1,   color: '#9aa7b8' }, // pipe
  { x: 7, y: 1, z: 1,   w: 2,  d: 3, h: 3,   color: '#e2a03f' }, // pump
  { x: 7.5, y: 4.5, z: 1, w: 1, d: 1, h: 0.6, color: '#d9534f' }  // valve
];

const body = [...parts].sort(order).map(box).join('\n  ');
process.stdout.write(`&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 460" width="600" height="460"&gt;
  &lt;rect width="600" height="460" fill="#0f1420"/&gt;
  ${body}
&lt;/svg&gt;
`);
</code></pre>
<p>To add a part, add one line to <code>parts</code>. To change the look, change the four multipliers in <code>tones</code>. Pick mid-tone base colours: <code>tones</code> clamps at 255, so a very light base loses the contrast between its top and right faces. On Windows PowerShell 5.1, <code>node iso.mjs | Out-File -Encoding utf8 machine.svg</code> avoids a UTF-16 file.</p>
<h2>Where to take it next</h2>
<p>Real explainer figures need more than boxes: cylinders and rounded parts, labels that never collide, cutaways that open a wall to show the inside, live numbers on gauges, and motion. Each of those is its own small project.</p>
<p>For detailed figures without building the renderer yourself, the open-source <a href="https://github.com/wheresryan22/anatomy">anatomy skill</a> (MIT) is an agent skill that has Claude Code create detailed, interactive isometric SVG illustrations of machines and systems. Its source is a good place to learn the techniques.</p>
<p><em>Disclosure: I'm the founder of Homingo; Homingo Learn is our learning brand. We built a paid Claude Code skill that includes that open-source skill with our reel and explainer kit on top. Isometric Explainers adds a 1080x1920 reel kit (camera moves, cutaways, callouts locked to parts, named cues synced to your narration), live readouts, and built-in layout, motion and geometry checks, so one figure becomes both a docs illustration and a vertical explainer reel. It's Rs 1,499 one-time: <a href="https://learn.homingo.co.in/agent-skills/isometric-explainers?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=isometric">Isometric Explainers for Claude Code</a>. Everything above works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[Tool calling in Python for Claude and OpenAI: the full agent loop]]></title><description><![CDATA[Tool calling (function calling) in Python for Claude and OpenAI: tool schemas, argument validation, readable errors, parallel calls and the full agent…]]></description><link>https://homingolearn.hashnode.dev/tool-calling-in-python-for-claude-and-openai-the-full-agent-loop</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/tool-calling-in-python-for-claude-and-openai-the-full-agent-loop</guid><category><![CDATA[Python]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 13:58:52 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://learn.homingo.co.in/guides/llm-tool-calling-python">Homingo Learn</a>.</em></p>
<p><em>Tool calling is how a model asks your code to do something (OpenAI calls it function calling; Anthropic calls it tool use). The model only proposes; your code checks the request, runs it and reports back.</em></p>
<h2>How tool calling works</h2>
<p>You send the model a list of tools, each with a name, a description and a JSON Schema for its arguments. When the model decides a tool would help, it replies with a <strong>tool call</strong> (a <code>tool_use</code> block in the Anthropic API) instead of, or alongside, text. Your code runs the tool and sends the result back in the next message (a <code>tool_result</code> block), and the model continues. With tools you define (client tools), the model never runs anything itself, which is why the checks belong in your code.</p>
<h2>Define tools and validate their arguments</h2>
<p>Keep each tool’s schema next to its function in a decorator, so the two are edited together. This minimal version makes every argument required (an optional third value lists the allowed values as an <code>enum</code>), and the runner turns any failure into a readable error for the model:</p>
<pre><code class="language-python">import json

TOOLS = {}


def tool(description, /, **params):
    """Register a function as a tool. params: name -&gt; (JSON type, description[, allowed values]); all required."""
    def prop(json_type, desc, enum=None):
        return {"type": json_type, "description": desc, **({"enum": enum} if enum else {})}

    def wrap(fn):
        TOOLS[fn.__name__] = {
            "fn": fn,
            "spec": {
                "name": fn.__name__,
                "description": description,
                "input_schema": {
                    "type": "object",
                    "properties": {k: prop(*v) for k, v in params.items()},
                    "required": list(params),
                    "additionalProperties": False,  # the runner rejects unknown keys, so the schema says so too
                },
            },
        }
        return fn
    return wrap


@tool("Current weather for a city. Use for any question about weather now.",
      city=("string", "City name, e.g. 'Lisbon'"))
def get_weather(city: str) -&gt; dict:
    return {"city": city, "temp_c": 19, "sky": "clear"}  # stand-in for a real API call


@tool("Convert an amount between EUR and USD (demo rates).",
      amount=("number", "Amount to convert"), source=("string", "Currency code", ["EUR", "USD"]),
      target=("string", "Currency code", ["EUR", "USD"]))
def convert(amount: float, source: str, target: str) -&gt; dict:
    rates = {("EUR", "USD"): 1.08, ("USD", "EUR"): 0.93}
    if (source, target) not in rates:
        raise ValueError(f"no rate for {source}-&gt;{target}; supported: EUR&lt;-&gt;USD")
    return {"amount": round(amount * rates[(source, target)], 2), "currency": target}


TYPES = {"string": str, "number": (int, float), "integer": int, "boolean": bool, "object": dict, "array": list}


def type_ok(value, json_type: str) -&gt; bool:
    if isinstance(value, bool) and json_type != "boolean":  # True is an int in Python, not in JSON Schema
        return False
    if json_type == "integer" and isinstance(value, float):  # JSON Schema accepts 50.0 as an integer
        return value.is_integer()
    return isinstance(value, TYPES[json_type])


def run_tool(call: dict) -&gt; dict:
    """Validate and run one tool call; argument and tool failures come back as error results."""
    entry = TOOLS.get(call["name"])
    if entry is None:
        return error(call, f"unknown tool {call['name']!r}; available: {', '.join(TOOLS)}")
    schema, args = entry["spec"]["input_schema"], call.get("input")
    if not isinstance(args, dict):
        return error(call, "arguments must be a JSON object")
    missing = [k for k in schema["required"] if k not in args]
    props = schema["properties"]
    wrong = [k for k, v in args.items() if k in props
             and (not type_ok(v, props[k]["type"]) or v not in props[k].get("enum", [v]))]
    unknown = [k for k in args if k not in schema["properties"]]
    if missing or wrong or unknown:
        return error(call, f"bad arguments: missing={missing} invalid={wrong} unknown={unknown}")
    try:
        content = json.dumps(entry["fn"](**args), default=str)  # dates, decimals etc. become strings
    except Exception as e:  # the model gets a readable error and can try again
        return error(call, f"{type(e).__name__}: {e}")
    return {"type": "tool_result", "tool_use_id": call["id"], "content": content}


def error(call, message):
    return {"type": "tool_result", "tool_use_id": call["id"], "content": message, "is_error": True}


print([t["spec"]["name"] for t in TOOLS.values()])
print(run_tool({"id": "t1", "name": "convert", "input": {"amount": 50, "source": "EUR", "target": "USD"}}))
print(run_tool({"id": "t2", "name": "convert", "input": {"amount": "50", "source": "EUR"}}))
print(run_tool({"id": "t3", "name": "convert", "input": {"amount": 5, "source": "EUR", "target": "EUR"}}))
# ['get_weather', 'convert']
# {'type': 'tool_result', 'tool_use_id': 't1', 'content': '{"amount": 54.0, "currency": "USD"}'}
# ... 'content': "bad arguments: missing=['target'] invalid=['amount'] unknown=[]", 'is_error': True}
# ... 'content': 'ValueError: no rate for EUR-&gt;EUR; supported: EUR&lt;-&gt;USD', 'is_error': True}
</code></pre>
<p>Two details do most of the work. Errors go back as a <code>tool_result</code> with <code>is_error: true</code> instead of crashing the loop, and they say what to do differently (“supported: EUR&lt;-&gt;USD”), so the model can correct itself on the next step. And the description is a routing rule: say when to use the tool, not just what it does. The check covers top-level names and types; for nested schemas use <code>jsonschema.validate(args, schema)</code> (catch <code>jsonschema.ValidationError</code> and return its message as an <code>is_error</code> result) or your provider’s strict schema mode.</p>
<h2>The agent loop and parallel tool calls with Claude (Anthropic API)</h2>
<pre><code class="language-python">from concurrent.futures import ThreadPoolExecutor


def chat_with_tools(call_model, user_text, max_steps=6):
    """The tool loop: call the model, run every tool it asks for, send the results back, repeat."""
    messages = [{"role": "user", "content": user_text}]
    specs = [t["spec"] for t in TOOLS.values()]
    for _ in range(max_steps):
        reply = call_model(messages, specs)
        if reply.get("stop_reason") == "max_tokens":  # cut off: a tool call or the answer may be incomplete
            return "Stopped: reply cut off (max_tokens)."
        messages.append({"role": "assistant", "content": reply["content"]})
        calls = [b for b in reply["content"] if b["type"] == "tool_use"]
        if not calls:  # no tool requested: this is the answer
            return "".join(b["text"] for b in reply["content"] if b["type"] == "text")
        with ThreadPoolExecutor() as pool:  # read-only calls run at the same time; map keeps their order
            results = list(pool.map(run_tool, calls))
        # Anthropic API: all results for one assistant turn go back together, in one user message.
        messages.append({"role": "user", "content": results})
    return "Stopped: too many tool steps."


# Stand-in model so the example runs offline. With the Anthropic SDK this is:
#   client.messages.create(model=..., max_tokens=1024, tools=specs, messages=messages).model_dump(exclude_none=True)
def fake_model(messages, specs):
    last = messages[-1]["content"]
    if isinstance(last, str):  # first turn: ask for two tools at once
        return {"stop_reason": "tool_use", "content": [
            {"type": "tool_use", "id": "a1", "name": "get_weather", "input": {"city": "Lisbon"}},
            {"type": "tool_use", "id": "a2", "name": "convert", "input": {"amount": 50, "source": "EUR", "target": "USD"}},
        ]}
    results = {r["tool_use_id"]: json.loads(r["content"]) for r in last}  # the demo assumes both succeed
    w, c = results["a1"], results["a2"]
    return {"stop_reason": "end_turn", "content": [{"type": "text", "text": f"{w['city']}: {w['temp_c']}°C and {w['sky']}. 50 EUR is {c['amount']} USD."}]}


print(chat_with_tools(fake_model, "Weather in Lisbon, and what is 50 EUR in dollars?"))
# Lisbon: 19°C and clear. 50 EUR is 54.0 USD.
</code></pre>
<p>The model asked for both tools in one turn. The loop runs them at the same time and returns all the results together in a single user message, each matched to its call by <code>tool_use_id</code>. Run read-only tools in parallel like this; run tools that change state one at a time, in order. The thread pool helps with I/O-bound tools such as API calls. The loop also checks <code>stop_reason</code>: a reply cut off at <code>max_tokens</code> can hold an incomplete tool call, so it is never run or returned as the answer. At the step cap this snippet returns a stop message; to get an answer instead, make one last call with <code>tool_choice={"type": "none"}</code> so the model must reply from what it has. The <code>max_steps</code> cap is the simplest guard against a model that keeps calling tools; see <a href="https://learn.homingo.co.in/guides/stop-ai-agent-infinite-loops-python">how to stop an AI agent infinite loop</a> for repeat detection and finish checks.</p>
<h2>OpenAI function calling in Python (Chat Completions and Responses)</h2>
<ul>
<li>Chat Completions: tools are <code>{"type": "function", "function": {"name": ..., "description": ..., "parameters": {...}}}</code>; <code>parameters</code> is the same JSON Schema as <code>input_schema</code> above.</li>
<li>Calls arrive in <code>message.tool_calls</code>, with the arguments as a JSON string to parse (<code>json.loads</code>, and treat a parse failure as a tool error).</li>
<li>Append the assistant message that carries <code>tool_calls</code> first, then each result as its own message with <code>role: "tool"</code> and the matching <code>tool_call_id</code>. Skipping the assistant message is the most common Chat Completions error.</li>
<li>Responses API: tools are flat (<code>{"type": "function", "name": ..., "parameters": {...}}</code>), calls arrive as <code>function_call</code> items and results go back as <code>function_call_output</code> items with the same <code>call_id</code>.</li>
</ul>
<p><em>The Chat Completions loop (openai SDK), reusing the tools and run_tool from above.</em></p>
<pre><code class="language-python">def chat_with_tools_openai(client, model, user_text, max_steps=6):
    """The same loop on OpenAI Chat Completions, reusing TOOLS and run_tool from above."""
    tools = [{"type": "function", "function": {"name": s["name"], "description": s["description"],
                                               "parameters": s["input_schema"]}}
             for s in (t["spec"] for t in TOOLS.values())]
    messages = [{"role": "user", "content": user_text}]
    for _ in range(max_steps):
        choice = client.chat.completions.create(model=model, messages=messages, tools=tools).choices[0]
        if choice.finish_reason == "length":
            return "Stopped: reply cut off (max_tokens)."
        msg = choice.message
        messages.append(msg)  # the assistant message carrying tool_calls goes in BEFORE the tool messages
        if not msg.tool_calls:
            return msg.content
        for tc in msg.tool_calls:  # one role "tool" message per call, matched by tool_call_id
            try:
                args = json.loads(tc.function.arguments)  # arguments arrive as a JSON string
            except json.JSONDecodeError:
                args = None  # run_tool answers "arguments must be a JSON object"
            result = run_tool({"id": tc.id, "name": tc.function.name, "input": args})
            messages.append({"role": "tool", "tool_call_id": tc.id, "content": result["content"]})
    return "Stopped: too many tool steps."
</code></pre>
<p><strong>Forcing or disabling tool calls.</strong> Both APIs let you steer the choice. Anthropic: <code>tool_choice</code> is <code>{"type": "auto"}</code> (the default), <code>{"type": "any"}</code> (must use some tool), <code>{"type": "tool", "name": "convert"}</code> (must use that one) or <code>{"type": "none"}</code>; add <code>"disable_parallel_tool_use": true</code> to get at most one call per turn. OpenAI: <code>tool_choice</code> is <code>"auto"</code>, <code>"required"</code>, <code>"none"</code> or a named function, and <code>parallel_tool_calls=False</code> turns parallel calls off.</p>
<h2>Writing tools the model uses well</h2>
<ul>
<li><strong>Few, distinct tools.</strong> Two tools that overlap (“search” and “lookup”) make the model guess. Merge them or make the descriptions say when each applies.</li>
<li><strong>Small, typed arguments.</strong> An enum or a number is easier to get right than a paragraph of open-ended text the tool must parse.</li>
<li><strong>Compact results.</strong> Return the fields the model needs, not the whole API response; every result is read back into the context.</li>
<li><strong>Read-only by default.</strong> Tools that change things (send, delete, pay) should ask a person first or run only in a sandbox.</li>
<li><strong>Retries live in the tool layer.</strong> A rate limit or a timeout should be retried by your code before the model sees an error. Retry only idempotent calls, or pass an idempotency key, so a send or a payment never runs twice; see <a href="https://learn.homingo.co.in/guides/llm-api-retries-rate-limits-python">retries and rate limits in Python</a>.</li>
</ul>
<h2>Common mistakes</h2>
<ul>
<li><strong>Trusting the arguments.</strong> The model can send a wrong type, a missing field or a made-up tool name. Validate before running.</li>
<li><strong>Raising inside the loop.</strong> One exception ends the whole run. Catch it in the runner and return it as a tool error.</li>
<li><strong>Losing a result.</strong> Every call needs its result: with the Anthropic API all results for one turn go back together in the next user message; with OpenAI, one tool message per call. A missing one is an API error.</li>
<li><strong>No step cap.</strong> A loop without one can run until the budget is gone.</li>
</ul>
<p>Tools are one layer of an agent, alongside the loop, memory and checks. See <a href="https://learn.homingo.co.in/guides/ai-agent-memory-python">AI agent memory in Python</a> and <a href="https://learn.homingo.co.in/guides/build-multi-agent-research-system-python#tools">the tool layer of a multi-agent research system</a>.</p>
<blockquote>
<p><strong>Multi-agent systems: build an AI research team</strong>: Six modules, each with a short video lesson, a quiz check and a hands-on coding round that adds a tested piece of the team to your own repo. Together they cover the planner contract, handoffs, the tool layer, shared memory, the source checker, budgets, traces and an eval gate. The capstone is your team running live on a topic of your choice, with its reports, eval scores and code in a public GitHub repo. For builders who know Python and have called a model API. Certificate on completion. <a href="https://learn.homingo.co.in/courses/multi-agent-research-team?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=multiagent-course">See the course</a></p>
</blockquote>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the course linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[CLAUDE.md best practices: what to include and skip, with a template]]></title><description><![CDATA[How to write a CLAUDE.md that Claude Code follows: where the file goes, what belongs in it, imports, path-scoped rules, how long it should be and a…]]></description><link>https://homingolearn.hashnode.dev/claude-md-best-practices-what-to-include-and-skip-with-a-template</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/claude-md-best-practices-what-to-include-and-skip-with-a-template</guid><category><![CDATA[claude-code]]></category><category><![CDATA[AI]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 13:56:29 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://learn.homingo.co.in/guides/claude-md-best-practices">Homingo Learn</a>.</em></p>
<p><em>CLAUDE.md is the note Claude Code loads into every session. Short, concrete and current, it saves you repeating yourself; long and vague, it costs context every session and is followed less reliably.</em></p>
<h2>What CLAUDE.md is (and is not)</h2>
<p><code>CLAUDE.md</code> is a Markdown file of standing instructions that Claude Code loads into context when a session starts. It is delivered as context, not as enforced configuration: Claude reads it and tries to follow it, and the more specific and short it is, the more reliably that happens. Anything that must happen every single time belongs in a <a href="https://learn.homingo.co.in/guides/claude-code-hooks-examples">hook</a> (format after every edit) or a permissions deny rule (never read <code>.env</code>) instead.</p>
<h2>Where CLAUDE.md files go</h2>
<ul>
<li><strong>Project:</strong> <code>./CLAUDE.md</code> or <code>./.claude/CLAUDE.md</code>, committed so the whole team shares it.</li>
<li><strong>Personal, all projects:</strong> <code>~/.claude/CLAUDE.md</code>.</li>
<li><strong>Personal, this project:</strong> <code>./CLAUDE.local.md</code> (add it to <code>.gitignore</code> yourself).</li>
<li><strong>Organisation:</strong> a managed policy file that IT deploys to every machine (for example <code>/etc/claude-code/CLAUDE.md</code> on Linux).</li>
</ul>
<p>Files in the folder you start Claude in and in every folder above it load at launch and are joined together, broadest first, so the instructions closest to your working folder are read last. A <code>CLAUDE.md</code> in a subfolder loads only when Claude reads or edits a file in that subfolder. Run <code>/context</code> to see which files loaded (they are listed under Memory files) and <code>/memory</code> to open them.</p>
<h2>How to create a CLAUDE.md with /init</h2>
<p>Run <code>/init</code> in your project: Claude reads the codebase and drafts a CLAUDE.md with the build and test commands and the conventions it finds. If one exists, it suggests improvements instead of overwriting it. Treat the draft as a starting point: cut what Claude could read from the code anyway and add what it could not know.</p>
<h2>What belongs in CLAUDE.md</h2>
<p>Write down what you would otherwise explain again: the second time Claude makes the same mistake, the correction you typed last session, the thing a new teammate would need on day one. In practice:</p>
<ul>
<li><strong>Commands:</strong> how to build, test, lint and run one test, with the exact command.</li>
<li><strong>Conventions that differ from the defaults:</strong> “use pnpm, not npm”, “API handlers live in <code>src/api/handlers/</code>”.</li>
<li><strong>Sources of truth:</strong> which file wins when two disagree, which files are generated and must not be edited.</li>
<li><strong>Pitfalls and their reasons:</strong> one line each, with the why, so Claude can apply it to cases you did not list.</li>
<li><strong>Boundaries:</strong> what to ask before doing (new dependencies, migrations, anything that deploys).</li>
</ul>
<p>Make each line checkable. “Use 2-space indentation” beats “format code properly”; “run <code>npm test</code> before committing” beats “test your changes”.</p>
<h2>What to skip in CLAUDE.md</h2>
<ul>
<li><strong>Anything Claude can read from the code:</strong> folder trees, dependency lists, descriptions it can rebuild in seconds. Keep the non-obvious architectural decisions and their reasons.</li>
<li><strong>Multi-step procedures and one-area knowledge:</strong> move them into a <a href="https://learn.homingo.co.in/guides/how-to-write-skill-md-claude-code">skill</a> (only its short description is always loaded; the body loads when needed) or a path-scoped rule.</li>
<li><strong>Rules that must never be broken:</strong> enforce them with permissions or hooks; a sentence can be missed.</li>
<li><strong>History and changelogs:</strong> what changed last month is git’s job.</li>
<li><strong>Secrets:</strong> never. Project files are committed, and anything in a memory file can end up in Claude’s context.</li>
</ul>
<h2>CLAUDE.md template and example</h2>
<p>The skeleton first, then a filled-in example. Keep the sections; swap the lines for your project.</p>
<p><em>CLAUDE.md skeleton</em></p>
<pre><code class="language-markdown"># &lt;Project name&gt;

## Commands
- `&lt;build command&gt;`: what it does
- `&lt;test command&gt;`: run after every change to &lt;area&gt;
- `&lt;single test command&gt;`: run one test file

## Sources of truth
- &lt;file or folder&gt; decides &lt;thing&gt;; never edit &lt;generated folder&gt;

## Conventions
- &lt;a rule that differs from the defaults, with the why&gt;

## Ask first
- &lt;actions that need a yes: dependencies, migrations, deploys&gt;

## Pitfalls
- &lt;the mistake Claude made twice, and the right way&gt;
</code></pre>
<p>Filled in for a Remotion repository that renders vertical videos from a spec file:</p>
<p><em>CLAUDE.md</em></p>
<pre><code class="language-markdown"># Vertical video repo

## Commands
- `npm run studio`: preview compositions in the browser
- `npm run render -- ep05`: render episodes/ep05 to out/ep05.mp4
- `npm test`: spec schema and caption length checks (run after every spec edit)

## Sources of truth
- episodes/&lt;id&gt;/spec.json decides every shot: change the spec, never the files in
  src/generated/ (rebuilt on each render)
- Output is 1080x1920 at 30 fps; captions stay inside the safe area in @docs/safe-area.md

## Conventions
- One React component per shot type in src/scenes/; props come only from the spec
- Colours come from src/theme.ts, never hard-coded

## Ask first
- Adding a dependency, changing the spec schema, deleting anything in episodes/

## Pitfalls
- out/ is gitignored (large files; every render rebuilds it): never force-add it
- If a render fails on a missing font or asset, report it; do not install anything
  or change the spec to work around it
</code></pre>
<p>Note the <code>@docs/safe-area.md</code> import: the referenced file is loaded with CLAUDE.md, so long reference material can live in its own file. Imports help organisation but do not save context, because the imported file loads every time.</p>
<h2>Imports and path-scoped rules</h2>
<p><code>@path/to/file</code> imports another file (relative to the importing file; up to four levels deep). For instructions that only matter in one part of the codebase, use <code>.claude/rules/</code>: one Markdown file per topic, and a <code>paths</code> list in the frontmatter makes a rule load only when Claude works on matching files.</p>
<p><em>.claude/rules/scenes.md</em></p>
<pre><code class="language-markdown">---
paths:
  - "src/scenes/**/*.tsx"
---

# Scene components
- Every animated value is driven by the frame number, never by timers
- Keep text inside the safe area described in docs/safe-area.md
</code></pre>
<h2>Does Claude Code read AGENTS.md?</h2>
<p>Yes, in recent versions: if a repository has an <code>AGENTS.md</code> and no <code>CLAUDE.md</code> or <code>CLAUDE.local.md</code> in the working folder or above it, Claude Code reads the <code>AGENTS.md</code>. Once a <code>CLAUDE.md</code> exists, it reads only the CLAUDE.md files by default. To keep one file for every coding tool, write the shared rules in <code>AGENTS.md</code> and put <code>@AGENTS.md</code> in your CLAUDE.md, with any Claude-only lines below it.</p>
<h2>How long should CLAUDE.md be? Size and upkeep</h2>
<ul>
<li><strong>Aim for under 200 lines per file.</strong> Longer files cost context in every session and lower adherence; Claude Code warns at startup when a file is over the recommended length.</li>
<li><strong>Remove contradictions.</strong> If two lines (or two files) disagree, Claude may pick either. Re-read the file when you add to it.</li>
<li><strong>Leave notes for humans in HTML comments.</strong> Block-level <code>&lt;!-- ... --&gt;</code> comments are stripped before the file reaches Claude, so they cost no tokens.</li>
<li><strong>Prune with each change.</strong> When a rule stops being true, delete it in the same commit that changed the code.</li>
</ul>
<h2>CLAUDE.md vs auto memory, skills, subagents and hooks</h2>
<p>Claude Code also keeps <strong>auto memory</strong>: notes Claude writes for itself about your preferences and corrections, in a <code>MEMORY.md</code> index per repository (the first 200 lines or 25KB load each session). Use the <code>/memory</code> command to browse it. The division of labour: CLAUDE.md for the rules you write, auto memory for what Claude learns, <a href="https://learn.homingo.co.in/guides/how-to-write-skill-md-claude-code">skills</a> for procedures and reusable prompts (<a href="https://learn.homingo.co.in/guides/claude-code-custom-slash-commands">custom slash commands</a> now live there too), <a href="https://learn.homingo.co.in/guides/claude-code-subagents-guide">subagents</a> for independent checks and <a href="https://learn.homingo.co.in/guides/claude-code-hooks-examples">hooks</a> for guarantees. After <code>/compact</code>, the project-root CLAUDE.md is read again from disk, so rules given only in chat are the ones that get lost: if it matters, write it down.</p>
<blockquote>
<p>Details follow the <a href="https://code.claude.com/docs/en/memory">official memory documentation</a> as of November 2026.</p>
</blockquote>
<p>If you make short vertical videos with Claude Code, the skill below packages a complete spec-first method for them: story, episode spec, camera, light and grade, a Remotion starter and pre-post QA.</p>
<blockquote>
<p><strong>Animated horror reels skill</strong>: Our whole playbook as a Claude Code agent skill: the hook-first story method, the episode spec template, the camera, light and grade system with a palette builder, a render-ready Remotion starter and the QA checks before you post. Make reels like ours. <a href="https://learn.homingo.co.in/skill?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=claude-md-best-practices">See the skill</a></p>
</blockquote>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the skill linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[Claude Code hooks: PreToolUse, PostToolUse and Stop examples]]></title><description><![CDATA[Claude Code hooks with copy-ready examples: block edits to .env, auto-format with Prettier, restrict a subagent's Bash commands and check tests on Stop.]]></description><link>https://homingolearn.hashnode.dev/claude-code-hooks-pretooluse-posttooluse-and-stop-examples</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/claude-code-hooks-pretooluse-posttooluse-and-stop-examples</guid><category><![CDATA[claude-code]]></category><category><![CDATA[AI]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 13:53:16 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://learn.homingo.co.in/guides/claude-code-hooks-examples">Homingo Learn</a>.</em></p>
<p><em>CLAUDE.md asks; a hook makes sure. How hooks work, the JSON they read and return, and five examples to adapt today.</em></p>
<h2>What are Claude Code hooks?</h2>
<p>A <strong>hook</strong> is a command that Claude Code runs at a fixed point in its lifecycle: before a tool call (<code>PreToolUse</code>), after one succeeds (<code>PostToolUse</code>), when Claude finishes its reply (<code>Stop</code>), when a session starts, and many more. Instructions in CLAUDE.md are context that Claude tries to follow; a hook runs every time, whatever the model decides. Use CLAUDE.md for judgement and hooks for anything that must always happen or must never happen.</p>
<p>The events you will use most:</p>
<ul>
<li><code>PreToolUse</code>: before a tool call; can allow, deny or ask, or rewrite the input.</li>
<li><code>PostToolUse</code>: after a tool call succeeds (<code>PostToolUseFailure</code> when it fails); can add context for Claude.</li>
<li><code>UserPromptSubmit</code>: when you send a prompt, before Claude sees it.</li>
<li><code>Stop</code> and <code>SubagentStop</code>: when Claude or a subagent finishes its reply; can send it back to work.</li>
<li><code>SessionStart</code> and <code>SessionEnd</code>: at the start (including after <code>/compact</code>) and end of a session.</li>
<li><code>Notification</code> and <code>PermissionRequest</code>: when Claude needs your attention or is about to ask for permission.</li>
<li><code>PreCompact</code>: before the conversation is compacted.</li>
</ul>
<h2>Where hooks live: settings.json and frontmatter</h2>
<p>Hooks go in a settings file: <code>~/.claude/settings.json</code> for all your projects, <code>.claude/settings.json</code> for one project (commit it to share with the team) or <code>.claude/settings.local.json</code> for yourself only. Subagents and skills can also carry hooks in their frontmatter, active only while they run. The structure has three levels: the event, a matcher group, and the commands to run.</p>
<p><em>.claude/settings.json</em></p>
<pre><code class="language-json">{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write", "timeout": 30 }
        ]
      }
    ]
  }
}
</code></pre>
<p>That one is the official auto-format example: after every <code>Edit</code> or <code>Write</code>, Prettier formats the file Claude touched. The <code>matcher</code> is a tool name, a list like <code>Edit|Write</code>, or a regular expression (<code>mcp__github__.*</code> matches every tool of an MCP server called <code>github</code>); <code>*</code> or an empty matcher means every tool. Type <code>/hooks</code> in a session to see what is configured and where each hook comes from. This one needs <code>jq</code> and breaks on paths with spaces; the Python hooks below avoid both.</p>
<h2>Hook input JSON, exit codes and JSON output</h2>
<p>Claude Code sends the event as JSON on the hook’s standard input. For tool events it includes <code>session_id</code>, <code>cwd</code>, <code>hook_event_name</code>, <code>tool_name</code> and <code>tool_input</code> (for <code>Edit</code> and <code>Write</code>, <code>tool_input.file_path</code> is always absolute); <code>PostToolUse</code> adds <code>tool_response</code>. The hook answers in one of two ways:</p>
<ul>
<li><strong>Exit codes.</strong> <code>0</code> = carry on. <code>2</code> = block: in <code>PreToolUse</code> the tool call does not run and your stderr text goes to Claude as the reason; in <code>PostToolUse</code> the tool already ran, so stderr reaches Claude as a warning. Any other code is a non-blocking error: the action proceeds. A policy hook that exits <code>1</code> blocks nothing.</li>
<li><strong>JSON on stdout</strong> (with exit <code>0</code>) for finer control. <code>PreToolUse</code> returns <code>hookSpecificOutput.permissionDecision</code> (<code>allow</code>, <code>deny</code> or <code>ask</code>) plus a reason; <code>PostToolUse</code> and <code>Stop</code> use a top-level <code>"decision": "block"</code> with a <code>reason</code>; <code>hookSpecificOutput.additionalContext</code> (with <code>hookEventName</code>) adds a note for Claude.</li>
</ul>
<blockquote>
<p>Hooks change quickly between Claude Code versions. Everything here follows the <a href="https://code.claude.com/docs/en/hooks">official hooks reference</a> as of November 2026; check it before relying on a rarer event or field. We write the scripts below in Python so they run the same on macOS, Linux and Windows, with no <code>jq</code> needed.</p>
</blockquote>
<h2>Example 1: block edits to protected files (PreToolUse)</h2>
<p><em>.claude/hooks/protect_files.py</em></p>
<pre><code class="language-python">import json
import sys
from pathlib import PurePosixPath

data = json.load(sys.stdin)
path = PurePosixPath(data.get("tool_input", {}).get("file_path", "").replace("\\", "/"))
name, parts = path.name, set(path.parts)
blocked = (
    (name == ".env" or (name.startswith(".env.") and name != ".env.example"))
    or name == "package-lock.json"
    or ".git" in parts
    or "secrets" in parts
)
if blocked:
    print(f"Blocked: {path} is protected. Ask the user to change it.", file=sys.stderr)
    sys.exit(2)  # exit 2 = block the call; stderr goes to Claude as the reason
sys.exit(0)
</code></pre>
<p><em>.claude/settings.json</em></p>
<pre><code class="language-json">{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/protect_files.py\"" }]
      }
    ]
  }
}
</code></pre>
<p><code>$CLAUDE_PROJECT_DIR</code> is the project root, so the hook works whatever folder Claude is in. Use <code>python3</code> on macOS or Linux if that is your interpreter’s name, or <code>py</code> on Windows. If your settings file already has a <code>"hooks"</code> object, add the event inside it rather than a second <code>"hooks"</code> key. Test it by asking Claude to add a comment to <code>.env</code>: the edit is refused and Claude sees the <code>Blocked:</code> line. A hook only guards the tools it matches, and this one stops edits only. Add <code>Read(./.env)</code> and <code>Edit(./.env)</code> (and the same for <code>./secrets/**</code>) to <code>permissions.deny</code> in your settings, so Claude can neither read the secrets into its context nor change them with its file tools. Bash remains the gap: narrow it with Bash deny rules and Claude Code’s Bash sandboxing (<code>/sandbox</code>).</p>
<h2>Example 2: restrict a subagent's Bash commands (PreToolUse allow-list)</h2>
<p>In our <a href="https://learn.homingo.co.in/guides/claude-code-subagents-guide#team">subagents guide</a> a <code>file-qa</code> agent measures a rendered vertical video with ffprobe. Its prompt says “run only ffprobe”, but a prompt is a request. A hook in the agent’s own frontmatter turns it into a rule, active only while that agent runs:</p>
<p><em>.claude/agents/file-qa.md, frontmatter</em></p>
<pre><code class="language-markdown">---
name: file-qa
description: Technical QA of a rendered vertical video file. Use after every render, before posting.
tools: Bash
model: haiku
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: 'python "$CLAUDE_PROJECT_DIR/.claude/hooks/only_ffprobe.py"'
---
</code></pre>
<p><em>.claude/hooks/only_ffprobe.py</em></p>
<pre><code class="language-python">import json
import shlex
import sys

data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")


def decide(decision, reason):
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": decision,
        "permissionDecisionReason": reason,
    }}))
    sys.exit(0)


if any(ch in command for ch in ";&amp;|&lt;&gt;`$\n\r{}*?[]~#()"):
    decide("deny", "One plain ffprobe command only: no pipes, redirects, chaining, substitutions, globs or braces.")
try:
    args = shlex.split(command)
except ValueError:
    decide("deny", "Could not parse the command.")
if not args or args[0] != "ffprobe":
    decide("deny", "This agent may run ffprobe only.")
if any(a in ("-o", "-report") for a in args[1:]):
    decide("deny", "ffprobe may not write files here (-o and -report are blocked).")
decide("allow", "Plain ffprobe probe.")
</code></pre>
<p>Rejecting shell metacharacters first matters: <code>ffprobe x.mp4; rm -rf renders</code> starts with ffprobe too. So does <code>ffprobe -o</code>, which writes its output to a file, hence the last check. Braces matter too: <code>ffprobe x.mp4 {-o,out.txt}</code> expands to <code>-o out.txt</code> before ffprobe runs, so they are refused with the other shell characters. A <code>deny</code> from a hook holds in every permission mode, even <code>bypassPermissions</code>. An <code>allow</code> skips the permission prompt but never overrides a deny rule in your settings.</p>
<h2>Example 3: send failing tests back to Claude before it stops (Stop)</h2>
<p><em>.claude/hooks/tests_before_stop.py</em></p>
<pre><code class="language-python">import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path

MAX_TRIES = 3
data = json.load(sys.stdin)
counter = Path(tempfile.gettempdir()) / f"claude-stop-{data.get('session_id', 'default')}.txt"
tries = int(counter.read_text()) if counter.exists() else 0

project = os.environ.get("CLAUDE_PROJECT_DIR") or data.get("cwd")
result = subprocess.run(
    "npm test --silent", shell=True, capture_output=True, text=True,
    cwd=project, env={**os.environ, "CI": "true"},  # CI=true keeps test runners out of watch mode
)
if result.returncode == 0 or tries &gt;= MAX_TRIES:
    counter.unlink(missing_ok=True)  # tests pass, or out of tries: let Claude stop
    sys.exit(0)
counter.write_text(str(tries + 1))
tail = (result.stdout + result.stderr)[-1500:]
reason = f"Tests fail (try {tries + 1} of {MAX_TRIES}). Fix them before finishing:\n{tail}"
print(json.dumps({"decision": "block", "reason": reason}))
</code></pre>
<p>When the suite fails, Claude gets the failing output as its next instruction instead of ending the turn, up to three times in a row; after that Claude may stop, the counter resets and you decide. The official docs suggest checking the <code>stop_hook_active</code> input field to avoid endless loops, but that allows only one retry; a small counter allows a few. Claude Code also overrides a Stop hook that blocks eight times in a row with no tool call in between. <code>Stop</code> fires whenever Claude finishes a reply, so keep the suite fast or run only the tests for what changed. Register it with a timeout longer than your suite takes (in seconds; Stop needs no matcher):</p>
<p><em>.claude/settings.json</em></p>
<pre><code class="language-json">{
  "hooks": {
    "Stop": [
      { "hooks": [{ "type": "command", "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/tests_before_stop.py\"", "timeout": 300 }] }
    ]
  }
}
</code></pre>
<h2>Example 4: add context after a tool runs (PostToolUse)</h2>
<p>A <code>PostToolUse</code> hook cannot undo anything, but it can tell Claude something. This one, registered for <code>Edit|Write</code>, warns Claude when it edits a generated file so the next change goes to the spec instead:</p>
<p><em>.claude/settings.json</em></p>
<pre><code class="language-json">{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "python \"$CLAUDE_PROJECT_DIR/.claude/hooks/generated_note.py\"" }]
      }
    ]
  }
}
</code></pre>
<p><em>.claude/hooks/generated_note.py</em></p>
<pre><code class="language-python">import json
import sys

data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "").replace("\\", "/")
if "/src/generated/" in path:
    print(json.dumps({"hookSpecificOutput": {
        "hookEventName": "PostToolUse",
        "additionalContext": "This file is generated from the episode spec and is overwritten on the next render. "
                             "Make the change in episodes/&lt;id&gt;/spec.json instead.",
    }}))
</code></pre>
<p>To re-inject key rules after the conversation is compacted, a <code>SessionStart</code> hook with the matcher <code>compact</code> can print them; plain stdout from <code>SessionStart</code> goes straight into Claude’s context.</p>
<h2>Example 5: a desktop alert when Claude needs you (Notification)</h2>
<p><em>~/.claude/settings.json (Linux)</em></p>
<pre><code class="language-json">{
  "hooks": {
    "Notification": [
      { "hooks": [{ "type": "command", "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" }] }
    ]
  }
}
</code></pre>
<p>Put it in your user settings so it works in every project. On macOS, use <code>osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'</code> as the command; the official hooks guide also has a PowerShell version for Windows.</p>
<h2>Debugging hooks and common mistakes</h2>
<ul>
<li><strong>Exit 1 for a policy.</strong> It only logs an error. Block with exit 2 or a <code>deny</code> decision.</li>
<li><strong>JSON that never parses.</strong> Stdout must be a single JSON object. A shell profile that echoes text on startup breaks it; so does building JSON by string concatenation. Use a JSON encoder.</li>
<li><strong>Fields at the wrong level.</strong> <code>permissionDecision</code> sits inside <code>hookSpecificOutput</code>; <code>decision</code> for Stop and PostToolUse is top level.</li>
<li><strong>Scripts that are not executable.</strong> On macOS and Linux a script called directly needs <code>chmod +x</code>; calling it as <code>python script.py</code> avoids that. On Windows, Claude Code runs command hooks through Git Bash; if it cannot find Git Bash, the hook fails.</li>
<li><strong>Slow hooks on hot events.</strong> <code>PostToolUse</code> runs after every matching call, in parallel when Claude makes parallel calls. Keep it to milliseconds or narrow the matcher.</li>
<li><strong>Treating hooks as a sandbox.</strong> A hook sees only the tools it matches, and string checks on commands can be bypassed. For hard limits, use permission rules and the Bash sandbox as well.</li>
</ul>
<p>Hooks complete the set: <a href="https://learn.homingo.co.in/guides/claude-md-best-practices">CLAUDE.md</a> holds the project rules, a <a href="https://learn.homingo.co.in/guides/how-to-write-skill-md-claude-code">skill</a> says how to do a job, <a href="https://learn.homingo.co.in/guides/claude-code-subagents-guide">subagents</a> check it with a fresh context, and hooks enforce the lines nobody may cross. If you make short vertical videos with Claude Code, the skill below packages the spec-first workflow that Examples 2 and 4 come from.</p>
<blockquote>
<p><strong>Animated horror reels skill</strong>: Our whole playbook as a Claude Code agent skill: the hook-first story method, the episode spec template, the camera, light and grade system with a palette builder, a render-ready Remotion starter and the QA checks before you post. Make reels like ours. <a href="https://learn.homingo.co.in/skill?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=claude-code-hooks-examples">See the skill</a></p>
</blockquote>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the skill linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[How to make AI horror Shorts with Claude Code: a step-by-step workflow]]></title><description><![CDATA[A step-by-step workflow for animated horror Shorts and Reels with Claude Code: hook on frame 0, beat sheet, episode spec, stills review, render and QA.]]></description><link>https://homingolearn.hashnode.dev/how-to-make-ai-horror-shorts-with-claude-code-a-step-by-step-workflow</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/how-to-make-ai-horror-shorts-with-claude-code-a-step-by-step-workflow</guid><category><![CDATA[youtube]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[video]]></category><category><![CDATA[content creation]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 13:07:46 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://learn.homingo.co.in/guides/ai-horror-shorts-claude-code">Homingo Learn</a>.</em></p>
<p><em>From a one-line idea to a checked 1080x1920 episode, with Claude Code doing the typing and you directing the story.</em></p>
<p>An AI horror Short is usually won or lost in its first two seconds. The art is rarely the problem; the story is not built for a feed. A viewer scrolling at speed gives you one frame to show that something is wrong, and every second after that has to earn the next. This guide is the workflow we use to plan and build animated horror episodes with <strong>Claude Code</strong>: story first, then a written spec, then shots, then checks. It works for Instagram Reels and YouTube Shorts alike, because both are vertical 1080x1920 video.</p>
<h2>1. Fix the Shorts format before the idea</h2>
<p>Decide the container first, because it shapes the story:</p>
<ul>
<li><strong>Length:</strong> 45 to 90 seconds. Shorter rarely holds a turn and a reveal; longer loses people on a phone.</li>
<li><strong>Frame:</strong> vertical 1080x1920 at 30 fps. Keep text and the key action in a central band, roughly 25% to 65% from the top, with a margin at the sides (wider on the right): the bottom third, a strip at the top and the right edge sit under the app’s caption and buttons.</li>
<li><strong>Narration:</strong> one narrator with short lines. The narration carries the story and the pictures confirm it.</li>
<li><strong>Series:</strong> plan episodes, not one-offs. A cliffhanger is one of the best growth tools a horror channel has.</li>
</ul>
<h2>2. Write frame 0 and the hook first</h2>
<p>Write the opening before anything else and judge it with the sound off. Frame 0 must already show the problem: not a title card, not a slow establishing shot. Three hook shapes work reliably for horror:</p>
<ul>
<li><strong>In the middle of it:</strong> open on the worst moment, then step back. “The knocking started again at 3:12.”</li>
<li><strong>The wrong detail:</strong> an ordinary room with one thing that should not be there, held long enough to be noticed.</li>
<li><strong>Stakes on the clock:</strong> a deadline the viewer can see, so every second of the episode is a second lost.</li>
</ul>
<p>The first spoken line should land inside two seconds and ask a question the viewer wants answered. If the hook needs explaining, it is not the hook yet.</p>
<h2>3. Build a horror beat sheet that keeps asking questions</h2>
<p>A feed viewer stays while they have an open question. So build the episode as a ladder: every beat answers one small question and opens a bigger one. A shape that fits 60 seconds:</p>
<ol>
<li><strong>Hook (0-3 s):</strong> the problem, on screen and in the first line.</li>
<li><strong>Want (3-10 s):</strong> what the main character is trying to do, in one concrete action.</li>
<li><strong>Ladder (10-40 s):</strong> three or four beats, each a new clue that makes things worse.</li>
<li><strong>Turn (around 40 s):</strong> the thing they believed is wrong.</li>
<li><strong>Scare and reveal (40-55 s):</strong> hold the frame a beat longer than feels comfortable just before it.</li>
<li><strong>Cliffhanger (last 3-5 s):</strong> one new question, not a summary.</li>
</ol>
<blockquote>
<p>Use two different scare mechanisms per episode (for example, something seen too late and something that moves when it should not). The same trick twice reads as a pattern, and patterns stop being scary.</p>
</blockquote>
<h2>4. Turn the story into a spec Claude Code can follow</h2>
<p>Claude Code is very good at turning a precise description into code and very easy to lead astray with a vague one. So the story becomes a written spec file before any scene is built: the story fields, the narration lines with rough times, and the shots. For every shot, write what the viewer <em>sees</em> and what they must <em>understand</em>. That second field is the one that saves episodes: if you cannot say what a shot is for, cut it.</p>
<p><em>A short episode spec (a made-up story, for format only). Paths are inside the project’s public/ folder.</em></p>
<pre><code class="language-json">{
  "title": "Room 4",
  "hook": "A hotel guest hears his own voice through the wall",
  "want": "Get out before the voice reaches his door",
  "cliffhanger": "The voice says his name from inside the room",
  "voice": "voice/room4.mp3",
  "lines": [
    { "t": 0.3, "text": "The voice in room 5 was mine." },
    { "t": 2.4, "text": "Same words. Same cough. One second behind me." }
  ],
  "shots": [
    {
      "id": "s01", "start": 0, "end": 2.4, "image": "art/s01.png",
      "sees": "A man pressed to a wallpapered wall, one ear flat against it",
      "understands": "He is listening to something impossible",
      "camera": "slow push-in", "light": "single bedside lamp, warm"
    },
    {
      "id": "s02", "start": 2.4, "end": 5.8, "image": "art/s02.png",
      "sees": "His mouth moving; the wall's shadow moving a beat later",
      "understands": "Something behind the wall copies him with a delay",
      "camera": "drift", "light": "lamp flickers once"
    }
  ]
}
</code></pre>
<h2>5. Gather the art and the voice</h2>
<p>The spec tells you exactly what each shot needs, so make or collect only that. Use your own drawings, commissioned art or generated images you have the rights to, saved as separate layers (background, subject, foreground) so a camera move can give depth. Record the narration yourself or use audio you have a licence for, and save both into the project’s <code>public/</code> folder under the paths the spec names.</p>
<h2>6. Direct Claude Code with the spec, not with adjectives</h2>
<p>Give Claude Code the spec file and ask for one step at a time. A prompt that works well:</p>
<pre><code class="language-text">Read episode/episode.json. Stage shots s01 and s02 in the Remotion project:
- one scene per shot, timed from start/end in the spec
- camera and light exactly as written in each shot
- captions from "lines", inside the band 25% to 65% from the top,
  with side margins (wider on the right)
Do not change the spec. When done, render one still per shot into out/stills/
and list any shot you could not stage as written.
</code></pre>
<p>Two habits matter more than the wording. First, when something looks wrong, change the spec and ask Claude to re-stage, rather than patching the scene manually; the spec stays the single source of truth. Second, ask for stills before full renders: a still per shot takes seconds and shows most problems.</p>
<h2>7. Make flat art feel deep and frightening</h2>
<ul>
<li><strong>Camera with a reason:</strong> a slow push-in builds dread; a slow drift keeps a held shot alive while the viewer searches the image; a sudden cut is saved for the scare.</li>
<li><strong>One motivated light:</strong> a lamp, a phone screen, a window. Darkness should have shape, not be a black fill.</li>
<li><strong>A small palette:</strong> pick three or four colours for the whole episode and give safety and threat their own temperature, then shift the balance as things get worse.</li>
<li><strong>Layers:</strong> separate foreground, subject and background so a camera move gives parallax. Depth is what makes 2D look expensive.</li>
</ul>
<h2>8. Check the story with a blind read</h2>
<p>You know the story, so you cannot judge whether the pictures tell it. Ask Claude Code to pass the still image files to a subagent (or open a fresh session) with no spec or script in its prompt, and ask it to describe, shot by shot, what happens and what it expects next. Compare that with the <em>understands</em> field of each shot. Every shot it misreads gets restaged. Two rounds is usually enough.</p>
<h2>9. Render, then run technical QA</h2>
<p>Render a short draft first, then the full episode, and check the file before it goes anywhere: 1080x1920, the right duration, an audio stream present, no frozen stretch longer than two seconds and no accidental black frames. Our <a href="https://learn.homingo.co.in/guides/remotion-claude-code-vertical-video">Remotion + Claude Code guide</a> has the full project code and the exact commands.</p>
<h2>10. Post it as a series</h2>
<ul>
<li>Pay off the last episode’s question in the first two seconds of the next one, then open a new one.</li>
<li>Keep each episode’s premise new: before you post, compare its hook, setting and reveal with your last few episodes, and change anything that repeats.</li>
</ul>
<blockquote>
<p><strong>Animated horror reels skill</strong>: Our whole playbook as a Claude Code agent skill: the hook-first story method, the episode spec template, the camera, light and grade system with a palette builder, a render-ready Remotion starter and the QA checks before you post. Make reels like ours. <a href="https://learn.homingo.co.in/skill?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=ai-horror-shorts-claude-code">See the skill</a></p>
</blockquote>
<p>Next step: read <a href="https://learn.homingo.co.in/guides/claude-code-skills-for-video">Claude Code skills for video</a> to package a workflow like this one so Claude follows it every time.</p>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the skill linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[Remotion + Claude Code: render 1080x1920 reels and Shorts from a spec]]></title><description><![CDATA[Set up Remotion for vertical 1080x1920 video, drive every shot from a JSON spec with Claude Code, review stills, render and check the file with ffprobe.]]></description><link>https://homingolearn.hashnode.dev/remotion-claude-code-render-1080x1920-reels-and-shorts-from-a-spec</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/remotion-claude-code-render-1080x1920-reels-and-shorts-from-a-spec</guid><category><![CDATA[Remotion]]></category><category><![CDATA[React]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[video]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 13:01:26 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://learn.homingo.co.in/guides/remotion-claude-code-vertical-video">Homingo Learn</a>.</em></p>
<p><em>Video as code: a spec file in, a checked vertical mp4 out, with Claude Code writing the React.</em></p>
<p><strong>Remotion</strong> renders video from React components: every frame is a function of the frame number. That makes it a natural fit for Claude Code, which writes React well and can re-render a change in seconds. The trick is to stop describing shots in chat and put them in a spec file that both you and Claude edit. This guide sets up a vertical project, wires it to a spec, and ends with a rendered, checked 1080x1920 mp4.</p>
<h2>1. Create the Remotion project</h2>
<p>You need Node.js 18 or newer. Remotion brings its own ffmpeg for rendering; install <code>ffmpeg</code> yourself only for the checks in step 8 (for the ffprobe ones, <code>npx remotion ffprobe</code> also works). Create a project (pick the <strong>Blank</strong> template, so its files do not clash with the code below) and open it in Claude Code:</p>
<pre><code class="language-bash">npx create-video@latest my-reels   # choose "Blank"
cd my-reels
npm install
npx remotion studio   # live preview in the browser
</code></pre>
<h2>2. The spec</h2>
<p>One JSON file holds everything that changes between episodes: the voice-over file, the narration lines with their start times, and the shots with their times, art and camera move. Art and audio live in <code>public/</code>. (Our <a href="https://learn.homingo.co.in/guides/ai-horror-shorts-claude-code#spec">horror Shorts workflow</a> adds story fields to the same file.)</p>
<p><em>episode/episode.json</em></p>
<pre><code class="language-json">{
  "voice": "voice/ep01.mp3",
  "lines": [
    { "t": 0.3, "text": "The voice in room 5 was mine." },
    { "t": 2.4, "text": "Same words. Same cough. One second behind me." }
  ],
  "shots": [
    { "id": "s01", "start": 0, "end": 2.4, "image": "art/s01.png", "camera": "slow push-in" },
    { "id": "s02", "start": 2.4, "end": 5.8, "image": "art/s02.png", "camera": "drift" }
  ]
}
</code></pre>
<h2>3. One vertical composition, sized from the spec</h2>
<p>Reels and Shorts are 1080x1920. Register one composition at that size and let the spec decide its length. (Importing JSON needs <code>"resolveJsonModule": true</code> in <code>tsconfig.json</code>.)</p>
<p><em>src/Root.tsx</em></p>
<pre><code class="language-tsx">import { Composition } from 'remotion';
import { Episode } from './Episode';
import spec from '../episode/episode.json';

const FPS = 30;
const seconds = Math.max(1, ...spec.shots.map((s) =&gt; s.end)); // at least 1 s, even with no shots yet

export const RemotionRoot = () =&gt; (
  &lt;Composition
    id="Episode"
    component={Episode}
    width={1080}
    height={1920}
    fps={FPS}
    durationInFrames={Math.round(seconds * FPS)}
    defaultProps={{ spec }}
  /&gt;
);
</code></pre>
<h2>4. Shots, captions and voice, all timed by the spec</h2>
<p>Each shot and each narration line becomes a <code>&lt;Sequence&gt;</code>, so changing a time in the spec moves it, with no component edits. Every shot moves from its first frame (a push-in when the spec asks for one, a slow sideways drift otherwise), captions sit inside the safe band, and the voice-over plays under everything. The composition is as long as the last shot, so make sure the last shot’s <code>end</code> covers the whole narration:</p>
<p><em>src/Episode.tsx</em></p>
<pre><code class="language-tsx">import { AbsoluteFill, Audio, Img, Sequence, interpolate, staticFile, useCurrentFrame, useVideoConfig } from 'remotion';

type Shot = { id: string; start: number; end: number; image: string; camera: string };
type Line = { t: number; text: string };
type Spec = { voice?: string; lines: Line[]; shots: Shot[] };

const ShotView = ({ shot, frames }: { shot: Shot; frames: number }) =&gt; {
  const frame = useCurrentFrame(); // counts from 0 inside each Sequence
  const p = interpolate(frame, [0, frames], [0, 1], { extrapolateRight: 'clamp' });
  // every shot moves: a push-in zooms 8%; a drift slides 24 px sideways inside a 4% overscan
  const transform = shot.camera === 'slow push-in'
    ? 'scale(' + (1 + 0.08 * p) + ')'
    : 'scale(1.04) translateX(' + (12 - 24 * p) + 'px)';
  return (
    &lt;AbsoluteFill style={{ transform }}&gt;
      &lt;Img src={staticFile(shot.image)} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /&gt;
    &lt;/AbsoluteFill&gt;
  );
};

// Safe band: 25% to 65% from the top, 6% margin left, 12% right (the like/share column).
const Caption = ({ text }: { text: string }) =&gt; (
  &lt;div style={{ position: 'absolute', top: '25%', bottom: '35%', left: '6%', right: '12%',
                display: 'flex', alignItems: 'center', justifyContent: 'center' }}&gt;
    &lt;div style={{ color: 'white', fontSize: 64, fontWeight: 800, lineHeight: 1.15, textAlign: 'center',
                  textShadow: '0 4px 18px rgba(0,0,0,.85)' }}&gt;{text}&lt;/div&gt;
  &lt;/div&gt;
);

export const Episode = ({ spec }: { spec: Spec }) =&gt; {
  const { fps, durationInFrames } = useVideoConfig();
  const at = (s: number) =&gt; Math.round(s * fps);
  return (
    &lt;AbsoluteFill style={{ backgroundColor: '#05060a' }}&gt;
      {spec.shots.map((shot) =&gt; {
        const frames = Math.max(1, at(shot.end) - at(shot.start)); // never a zero-length shot
        return (
          &lt;Sequence key={shot.id} from={at(shot.start)} durationInFrames={frames}&gt;
            &lt;ShotView shot={shot} frames={frames} /&gt;
          &lt;/Sequence&gt;
        );
      })}
      {spec.lines.map((line, i) =&gt; {
        const end = i + 1 &lt; spec.lines.length ? at(spec.lines[i + 1].t) : durationInFrames;
        return (
          &lt;Sequence key={i} from={at(line.t)} durationInFrames={Math.max(1, end - at(line.t))}&gt;
            &lt;Caption text={line.text} /&gt;
          &lt;/Sequence&gt;
        );
      })}
      {spec.voice ? &lt;Audio src={staticFile(spec.voice)} /&gt; : null}
    &lt;/AbsoluteFill&gt;
  );
};
</code></pre>
<blockquote>
<p>Keep motion in <code>transform</code> and <code>opacity</code>, and give every shot some movement from its first second. A frame that does not change for more than two seconds reads as a frozen video on a phone, and fails the freeze check in step 8.</p>
</blockquote>
<h2>5. Let Claude Code edit the spec, then the code</h2>
<p>The working loop that keeps projects tidy:</p>
<ol>
<li>You describe the change in story terms (“the reveal comes too early, hold the empty corridor for another second”).</li>
<li>Claude edits <code>episode/episode.json</code> first and shows you the diff.</li>
<li>Only if the spec cannot express it does Claude touch a component, and then in a way the spec can control next time (a new field, not a hard-coded value).</li>
<li>Claude renders stills of the changed shots for you to look at before any full render.</li>
</ol>
<p>Put this loop in a <a href="https://learn.homingo.co.in/guides/claude-code-skills-for-video">Claude Code skill</a> and you never have to explain it again.</p>
<h2>6. Review stills before you render</h2>
<p>A still per shot takes seconds; a full render takes minutes. Render the middle frame of each shot: (start + end) / 2 × fps. For s01 (0 to 2.4 s) that is 1.2 s, frame 36; for s02 (2.4 to 5.8 s), frame 123. Look at them in order, or give only the image files to a separate subagent and ask it to tell the story back from the pictures alone:</p>
<pre><code class="language-bash">npx remotion still src/index.ts Episode out/stills/s01.png --frame=36
npx remotion still src/index.ts Episode out/stills/s02.png --frame=123
</code></pre>
<h2>7. Render the vertical video</h2>
<pre><code class="language-bash">npx remotion render src/index.ts Episode out/episode.mp4 --codec=h264 --crf=18
# on a small laptop, also limit parallel frames:
npx remotion render src/index.ts Episode out/episode.mp4 --codec=h264 --crf=18 --concurrency=2
</code></pre>
<p>The first still or render downloads a headless browser, so it is slower. Render one episode at a time on a laptop; parallel renders compete for memory.</p>
<h2>8. Check the file before you post</h2>
<p>Four commands catch most broken uploads:</p>
<pre><code class="language-bash"># size, frame rate and duration: expect 1080, 1920, 30/1
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate \
  -show_entries format=duration -of default=nw=1 out/episode.mp4

# audio present? (empty output = no audio track)
ffprobe -v error -select_streams a -show_entries stream=codec_name -of csv=p=0 out/episode.mp4

# frozen stretches of 2 s or more
ffmpeg -hide_banner -i out/episode.mp4 -vf "freezedetect=n=-60dB:d=2" -map 0:v:0 -f null - 2&gt;&amp;1 | grep -E "freeze_|rror|No such file"

# black frames of 0.5 s or more
ffmpeg -hide_banner -i out/episode.mp4 -vf "blackdetect=d=0.5:pix_th=0.10" -map 0:v:0 -f null - 2&gt;&amp;1 | grep -E "black_|rror|No such file"
</code></pre>
<ul>
<li>A <code>freeze_start</code> with no <code>freeze_duration</code> after it means the video stays frozen to the end: count it as a fail.</li>
<li>Low-key horror scenes on a dark background can be reported as black. Look at the reported timestamps before you change anything, or lower <code>pix_th</code> (for example to 0.05).</li>
<li>If an ffmpeg command prints an error instead of results, the file did not decode: that is a fail, not a pass.</li>
</ul>
<p>Ask Claude Code to run them and read every line before it says the file is ready. The <a href="https://learn.homingo.co.in/guides/claude-code-skills-for-video#example">reel QA skill</a> in our skills guide wraps these checks.</p>
<h2>Where to go from here</h2>
<p>This setup gives you the pipeline. What makes people stop scrolling is the story and the look: the hook on frame 0, the beats, the camera, light and grade. Our <a href="https://learn.homingo.co.in/guides/ai-horror-shorts-claude-code">horror Shorts workflow</a> covers the method, and the skill below packages all of it with a Remotion starter that renders out of the box.</p>
<blockquote>
<p><strong>Animated horror reels skill</strong>: Our whole playbook as a Claude Code agent skill: the hook-first story method, the episode spec template, the camera, light and grade system with a palette builder, a render-ready Remotion starter and the QA checks before you post. Make reels like ours. <a href="https://learn.homingo.co.in/skill?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=remotion-claude-code-vertical-video">See the skill</a></p>
</blockquote>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the skill linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[Claude Code skills for video: how they work and how to write one for vertical reels]]></title><description><![CDATA[What a Claude Code agent skill is, where it lives, how Claude decides to use it, and a working example skill that checks vertical reels before you post.]]></description><link>https://homingolearn.hashnode.dev/claude-code-skills-for-video-how-they-work-and-how-to-write-one-for-vertical-reels</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/claude-code-skills-for-video-how-they-work-and-how-to-write-one-for-vertical-reels</guid><category><![CDATA[claude-code]]></category><category><![CDATA[video]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 12:53:41 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://learn.homingo.co.in/guides/claude-code-skills-for-video">Homingo Learn</a>.</em></p>
<p><em>A skill turns a workflow you explain every time into one Claude Code follows on its own. Here is how, with a video example.</em></p>
<p>If you make videos with Claude Code, you have probably typed the same instructions many times: the format, the order of work, the checks to run before calling it done. A <strong>skill</strong> is where those instructions live once. Claude Code keeps a short list of every installed skill, recognises when a request matches one, and loads the full instructions only then. This guide explains how Claude Code skills work and walks through a small, real skill for checking vertical reels.</p>
<h2>What a Claude Code skill is</h2>
<p>A skill is a folder with a <code>SKILL.md</code> file in it. The file starts with YAML frontmatter (a <code>description</code> is the one field you should always write) and continues with plain Markdown instructions. Next to it you can put reference files, templates and scripts. The two main places Claude Code looks for skills:</p>
<ul>
<li><code>~/.claude/skills/&lt;skill-name&gt;/SKILL.md</code>: personal, available in every project on your computer.</li>
<li><code>.claude/skills/&lt;skill-name&gt;/SKILL.md</code>: inside one repository, shared with everyone who works in it.</li>
</ul>
<p>Only each skill’s name and description are always in Claude’s context. The body of <code>SKILL.md</code> loads when the skill is used, and the other files load only when the instructions point to them. That is why a skill can hold a whole method without slowing down every conversation. You can also run a skill yourself by typing <code>/skill-name</code>.</p>
<h2>The description does the routing</h2>
<p>Claude decides whether to use a skill mainly from its description, so write it the way a user would ask. Lead with the job, then list the phrases that should trigger it. Weak: “Video helper.” Strong: “Check a rendered vertical video (reel or Short) before posting: resolution, duration, audio, frozen and black frames. Use when the user asks to check, QA or validate an mp4.” Keep the most important use case first: the skill list truncates description text past 1,536 characters.</p>
<h2>How to create a Claude Code skill: a reel QA example</h2>
<p>This skill checks a rendered mp4 against the format Reels and Shorts expect. It needs <code>ffmpeg</code> and <code>ffprobe</code> on your path and a bash shell (on Windows, Git Bash or WSL). Folder layout:</p>
<pre><code class="language-text">~/.claude/skills/reel-qa/
  SKILL.md
  scripts/
    qa.sh
</code></pre>
<p><em>SKILL.md</em></p>
<pre><code class="language-markdown">---
name: reel-qa
description: Check a rendered vertical video (Instagram Reel or YouTube Short) before posting - resolution 1080x1920, frame rate, duration, audio present, frozen stretches and black frames. Use when the user asks to check, QA, validate or "make sure" an mp4 is ready to post.
---

# Reel QA

Run the checks, read every line of their output, then report.

1. Run scripts/qa.sh from this skill's folder (~/.claude/skills/reel-qa/ for a
   personal install, .claude/skills/reel-qa/ in a project):
   bash &lt;skill folder&gt;/scripts/qa.sh &lt;file.mp4&gt;
   If it prints FAIL and stops, report that error; do not guess the other checks.
2. The file passes only if ALL of these hold:
   - width 1080 and height 1920
   - r_frame_rate 30/1, or the rate the user says they rendered at
   - duration between 45 and 90 seconds (ask the user if their target differs)
   - at least one audio stream
   - no freeze_duration of 2 seconds or more, and no freeze_start without a
     freeze_duration after it (that means frozen through the end)
   - no black_duration of 0.5 seconds or more, except in the last second
3. Very dark scenes can be counted as black. When a black stretch is reported,
   give its timestamps so the user can look at those frames before deciding.
4. Report a table: check, value, PASS/FAIL. For each FAIL, say the likely cause
   and the smallest fix. Never report PASS for a check whose output you did not read.
</code></pre>
<p><em>scripts/qa.sh</em></p>
<pre><code class="language-bash">#!/usr/bin/env bash
set -uo pipefail
[ $# -eq 1 ] || { echo "usage: qa.sh &lt;file.mp4&gt;"; exit 2; }
f="$1"
log="$(mktemp)"
trap 'rm -f "$log"' EXIT
echo "== video"
ffprobe -v error -select_streams v:0 -show_entries stream=width,height,r_frame_rate \
  -show_entries format=duration -of default=nw=1 "$f" || { echo "FAIL: ffprobe could not read $f"; exit 1; }
echo "== audio streams (empty = none)"
ffprobe -v error -select_streams a -show_entries stream=codec_name -of csv=p=0 "$f"
echo "== frozen stretches (2 s or more) and black frames (0.5 s or more)"
if ! ffmpeg -hide_banner -nostats -i "$f" -vf "freezedetect=n=-60dB:d=2,blackdetect=d=0.5:pix_th=0.10" \
     -map 0:v:0 -f null - 2&gt; "$log"; then
  echo "FAIL: ffmpeg could not decode the video"; tail -5 "$log"; exit 1
fi
grep -E "freeze_(start|duration)|black_start" "$log" || echo "none found"
</code></pre>
<p>Save both files, start a new Claude Code session and ask “is out/episode.mp4 ready to post?”. Claude matches the request to the description, loads the skill and runs the script (it may ask your permission to run bash the first time).</p>
<h2>What makes a video skill good</h2>
<ul>
<li><strong>An order of work.</strong> Video goes wrong when steps are skipped. Number them: story, spec, stills, render, checks.</li>
<li><strong>Gates with exact pass rules.</strong> “Looks fine” is not a rule; “no frozen stretch of 2 s or more” is.</li>
<li><strong>“Read the output” in so many words.</strong> Tell Claude never to report a gate as passed without reading its output, and to stop on a tool error instead of guessing.</li>
<li><strong>Reference files for the long parts.</strong> Keep <code>SKILL.md</code> short (the official guidance is under 500 lines) and move the story method, spec format and style rules into separate files it points to.</li>
<li><strong>A separate reviewer.</strong> For story and look, ask Claude to have a subagent review stills without the script, so the reviewer sees only what a viewer sees.</li>
<li><strong>A starter project.</strong> Scripts and a project that render out of the box turn advice into something Claude can run.</li>
</ul>
<h2>From a QA skill to a whole production skill</h2>
<p>The QA skill above is the last step of a longer chain. A production skill for short-form video adds the story method (hook, beats, cliffhangers), a spec format that every shot is written in, a camera, light and colour approach, and a starter project that renders vertical video, with checks at each step. That is exactly how our horror reel skill is built: one <code>SKILL.md</code>, seven reference guides and a Remotion starter with one npm command per check. Our <a href="https://learn.homingo.co.in/guides/ai-horror-shorts-claude-code">horror Shorts workflow</a> shows the method it follows.</p>
<blockquote>
<p><strong>Animated horror reels skill</strong>: Our whole playbook as a Claude Code agent skill: the hook-first story method, the episode spec template, the camera, light and grade system with a palette builder, a render-ready Remotion starter and the QA checks before you post. Make reels like ours. <a href="https://learn.homingo.co.in/skill?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=claude-code-skills-for-video">See the skill</a></p>
</blockquote>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the skill linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[Claude Code subagents: how to create and use them (with examples)]]></title><description><![CDATA[Create Claude Code subagents step by step: the agent file and its fields, how Claude picks one, tool limits, fresh context, and reviewer agents for a…]]></description><link>https://homingolearn.hashnode.dev/claude-code-subagents-how-to-create-and-use-them-with-examples</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/claude-code-subagents-how-to-create-and-use-them-with-examples</guid><category><![CDATA[claude-code]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 12:43:06 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on <a href="https://learn.homingo.co.in/guides/claude-code-subagents-guide">Homingo Learn</a>.</em></p>
<p><em>A Claude Code subagent is a second Claude with its own context, tools and instructions. Used well, it keeps your main session clean and gives you a reviewer that did not write the work.</em></p>
<h2>What is a Claude Code subagent?</h2>
<p>A Claude Code <strong>subagent</strong> is a separate Claude instance that your main session passes a task to. It starts with a fresh context window, its own system prompt and, if you say so, a restricted set of tools. It works on the task, and only its final summary comes back into your conversation. That one property explains when subagents help: anything that reads a lot (logs, test output, a folder of frames) and anything that should be judged by someone who did not do the work.</p>
<h2>How to create a Claude Code subagent</h2>
<p>The usual way is one Markdown file with YAML frontmatter. Put it in <code>.claude/agents/</code> to share it with everyone working in the repository, or in <code>~/.claude/agents/</code> to have it in all your projects; if both define the same name, the project one wins. (Subagents can also be passed as JSON with the <code>--agents</code> flag, or come with a plugin.) Write the file yourself, or describe the agent you want and ask Claude to write it; depending on your version, <code>/agents</code> also opens a menu for this. Claude Code picks up edits to existing agent files within seconds; a newly created <code>agents</code> folder needs a restart of the session.</p>
<p><em>.claude/agents/stills-reviewer.md</em></p>
<pre><code class="language-markdown">---
name: stills-reviewer
description: Reviews storyboard stills of a vertical video against its episode spec. Use proactively after stills are rendered and before the full render.
tools: Read, Glob, Grep
model: sonnet
---

You review storyboard stills for a 1080x1920 short video. You did not write the
spec or make the stills.

Inputs: the episode spec (spec.json) and a folder of stills, one per shot.
Safe area: everything except the top 15% and the bottom 35% of the height and
6% at each side, where platform buttons and captions sit.

For every shot:
1. Look at the still first and write one sentence: what a first-time viewer
   would understand from it alone.
2. Then read the shot's line in the spec.
3. FAIL the shot if your sentence does not match the spec's intent, if the
   subject or any caption is outside the safe area, or if a caption is unreadable.

Return a table (shot id, PASS or FAIL, one-line reason) followed by one line
with the PASS and FAIL totals, and nothing else. Never edit files.
</code></pre>
<p>Only <code>name</code> and <code>description</code> are required. The fields you will use most:</p>
<ul>
<li><code>name</code>: the unique identifier you call the agent by. Lowercase words joined by hyphens are the safe convention.</li>
<li><code>description</code>: when Claude should pass work to this agent. It is the routing rule, so write it like the request it should catch.</li>
<li><code>tools</code>: an allowlist, comma-separated or as a YAML list. Leave it out and the agent inherits every tool available to subagents. A reviewer that only has <code>Read, Glob, Grep</code> cannot change anything by accident. <code>disallowedTools</code> is the opposite: a denylist.</li>
<li><code>model</code>: <code>sonnet</code>, <code>opus</code>, <code>haiku</code>, a full model ID, or <code>inherit</code> (the main session’s model). Routine checks run well on a smaller, cheaper model.</li>
<li><code>maxTurns</code>, <code>permissionMode</code>, <code>hooks</code>, <code>skills</code> (skills preloaded into the agent) and <code>isolation: worktree</code> (work in a temporary git worktree) cover the rarer cases.</li>
</ul>
<blockquote>
<p>Claude Code changes quickly. The fields and flags in this guide follow the <a href="https://code.claude.com/docs/en/sub-agents">official subagents documentation</a> as of November 2026; check there before relying on a rarer one.</p>
</blockquote>
<h2>How to use a subagent: automatic and explicit delegation</h2>
<p>Claude delegates on its own when a task matches an agent’s <code>description</code>; phrases like “use proactively” make that more likely. You can also be explicit: ask in plain words (“Use the stills-reviewer subagent on renders/ep04/stills against episodes/ep04/spec.json”), or type <code>@</code> and pick the agent from the list, which guarantees that agent runs. To make one agent the whole session, start Claude Code with <code>claude --agent stills-reviewer</code>.</p>
<h2>Fresh context: what the subagent does not know</h2>
<p>A subagent does not see your conversation or the files your main session already read. It gets its own system prompt, the delegation message Claude writes for it, your CLAUDE.md files and the environment basics. Two consequences:</p>
<ul>
<li><strong>Put the method in the agent file,</strong> not in the chat. Anything the agent needs every time (what to check, what a FAIL is, what to return) belongs in its prompt.</li>
<li><strong>Ask for a short, fixed return format.</strong> The summary is all that comes back. A table of PASS and FAIL lines costs your main session a few hundred tokens; a raw dump of everything the agent read costs thousands and defeats the point.</li>
</ul>
<h2>Example: three reviewer subagents that check a reel</h2>
<p>Short-form video is a good fit, because each check is independent and each one reads a lot. Three subagents, each with the fewest tools it needs, each used at its own stage: <code>story-reader</code> judges the spec before any pictures exist, <code>stills-reviewer</code> (above) judges the storyboard stills, <code>file-qa</code> measures the rendered file.</p>
<p><em>.claude/agents/story-reader.md</em></p>
<pre><code class="language-markdown">---
name: story-reader
description: Reads an episode spec as a first-time viewer and checks the story. Use before stills are made.
tools: Read
model: sonnet
---

Read spec.json once, as a stranger who sees only what is on screen and in the captions.
Answer each with PASS or FAIL and one line of reason:
1. Hook: does the first shot show a situation that raises a question?
2. Beats: does every shot add information or raise a new question?
3. Payoff: does the ending answer the question the hook raised?
4. Clarity: can every shot be described as one readable action?
Then one line with the totals, and nothing else. Never edit files.
</code></pre>
<p><em>.claude/agents/file-qa.md</em></p>
<pre><code class="language-markdown">---
name: file-qa
description: Technical QA of a rendered vertical video file. Use after every render, before posting.
tools: Bash
model: haiku
---

Run only ffprobe. For the file you are given (replace FILE with its path):
  ffprobe -v error -show_entries stream=codec_type,width,height,r_frame_rate:format=duration -of json "FILE"
Report the measured values, then PASS only if: a video stream is 1080x1920,
its r_frame_rate is 30/1, the duration is between 15 and 90 seconds, and an
audio stream exists. One line per check, then PASS or FAIL overall.
</code></pre>
<p>Set the frame rate and duration in <code>file-qa</code> to your own targets. Note that a prompt saying “run only ffprobe” is a request, not a limit: an agent with <code>Bash</code> can run anything Bash can. Keep permission prompts on for it (no <code>bypassPermissions</code>), or add a <code>PreToolUse</code> hook to the agent that rejects every Bash command that is not <code>ffprobe</code>. The other two agents have no Bash at all, so they cannot change a file whatever their prompt says.</p>
<p>Each reviewer then runs at its stage: story-reader once the spec is written, stills-reviewer once the stills exist. After the render, one instruction runs the final pass: “Run stills-reviewer and file-qa on episode 4 in parallel and give me every FAIL.” Independent agents can run at the same time, and you read short reports instead of folders of output. Fix the FAILs in the main session, where the context about the episode lives, then run the reviewers again.</p>
<h2>Subagents or skills?</h2>
<ul>
<li><strong>A skill</strong> is a method the working session loads when it is relevant (only its description is always in context): steps, reference files and scripts. Use it for <em>how</em> to do a job. See <a href="https://learn.homingo.co.in/guides/how-to-write-skill-md-claude-code">how to write a SKILL.md</a>.</li>
<li><strong>A subagent</strong> is a separate worker with its own context. Use it to keep heavy reading out of your session, to run independent checks in parallel, or to get a review from someone who did not write the work.</li>
<li><strong>Together:</strong> a production skill does the work in the main session; reviewer subagents check it. If a reviewer needs shared rules, preload only the checking part (a QA skill) through the <code>skills</code> field, not the production method, so it still judges as an outsider.</li>
</ul>
<h2>Common mistakes</h2>
<ul>
<li><strong>Every tool for every agent.</strong> A reviewer with write access will eventually “fix” what it was asked to judge. Restrict it.</li>
<li><strong>A subagent for a two-line job.</strong> Each one starts cold and has to gather its own context; small, quick edits are faster in the main session.</li>
<li><strong>Expecting it to remember the chat.</strong> If the agent needs a decision you made earlier, it goes in the delegation message or in the agent file.</li>
<li><strong>Long descriptions.</strong> Every agent’s description sits in the main session’s context. Keep it to the trigger; move the method into the body.</li>
</ul>
<p>For the production side of the same workflow, see <a href="https://learn.homingo.co.in/guides/ai-horror-shorts-claude-code">how to make AI horror Shorts with Claude Code</a> and <a href="https://learn.homingo.co.in/guides/claude-code-skills-for-video">Claude Code skills for video</a>.</p>
<blockquote>
<p><strong>Animated horror reels skill</strong>: Our whole playbook as a Claude Code agent skill: the hook-first story method, the episode spec template, the camera, light and grade system with a palette builder, a render-ready Remotion starter and the QA checks before you post. Make reels like ours. <a href="https://learn.homingo.co.in/skill?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=claude-code-subagents-guide">See the skill</a></p>
</blockquote>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the skill linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[Retry OpenAI and Claude API calls in Python: 429s, 529s, backoff, jitter]]></title><description><![CDATA[Handle OpenAI and Claude API 429 and 529 errors in Python: what to retry, backoff with jitter, Retry-After, a token bucket limiter, timeouts and…]]></description><link>https://homingolearn.hashnode.dev/retry-openai-and-claude-api-calls-in-python-429s-529s-backoff-jitter</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/retry-openai-and-claude-api-calls-in-python-429s-529s-backoff-jitter</guid><category><![CDATA[Python]]></category><category><![CDATA[llm]]></category><category><![CDATA[openai]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 12:23:08 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://learn.homingo.co.in/guides/llm-api-retries-rate-limits-python"><em>Homingo Learn</em></a><em>.</em></p>
<p><em>Agents make many model calls, and some of them will fail. The difference between a flaky agent and a dependable one is mostly in how those failures are handled.</em></p>
<p>An agent that makes hundreds of model calls per run will hit rate limits (429), overloaded servers (Anthropic’s 529) and dropped connections. Retrying blindly makes it worse: a tight loop of retries after a 429 just earns more 429s. This guide builds the pieces in plain Python 3.10 or newer: a clear rule for what to retry, backoff with jitter that respects <code>Retry-After</code>, a client-side rate limiter, timeouts, and a guard for actions that must never run twice. Apart from the SDK example, the snippets go in one file, in order.</p>
<h2>What to retry, and what not to</h2>
<ul>
<li><strong>Retry:</strong> <code>429</code> (rate limited), <code>408</code> (request timeout), <code>409</code> (conflict; the official SDKs retry it), <code>500</code>, <code>502</code>, <code>503</code>, <code>504</code>, and <code>529</code> (Anthropic’s “overloaded”). Also dropped connections and timeouts.</li>
<li><strong>Do not retry:</strong> <code>400</code> (your request is wrong), <code>401</code> and <code>403</code> (keys and permissions), <code>404</code>, <code>413</code> (too large), and TLS certificate errors. Sending the same request again gives the same answer; fix the request instead.</li>
</ul>
<h2>If you use the Anthropic or OpenAI SDK</h2>
<p>The official Python SDKs already retry these errors on their own (twice by default) and have their own long default timeouts. Either rely on that and tune it, or switch it off and use the code below, so failures are not retried twice over. This adapter uses <code>ApiError</code> and <code>retry_after_seconds</code> from the next section, and is called as <code>await call_with_retries(lambda: send_sdk(prompt), limiter)</code>:</p>
<pre><code class="language-python">import anthropic

client = anthropic.Anthropic(max_retries=0, timeout=120.0)

def send_sdk(prompt: str):
    try:
        return client.messages.create(model="your-model", max_tokens=800,
                                      messages=[{"role": "user", "content": prompt}])
    except anthropic.APIStatusError as e:  # map SDK errors onto the retry rule below
        raise ApiError(e.status_code, retry_after_seconds(e.response.headers)) from e
    except anthropic.APIConnectionError as e:  # includes timeouts
        raise ConnectionError(str(e)) from e

# OpenAI: the same pattern with openai.OpenAI(max_retries=0, timeout=120.0),
# client.chat.completions.create(...), openai.APIStatusError and openai.APIConnectionError.
</code></pre>
<p>Libraries such as tenacity can do the backoff part for you. The rate limiter and the idempotency guard below are still yours to add.</p>
<h2>Calling the LLM API with urllib: timeouts and error types</h2>
<p>Without an SDK, start with a small wrapper that turns every failure into one of three outcomes: an <code>ApiError</code> with the status, the provider’s error text and its retry hint; a <code>ConnectionError</code> worth retrying; or an exception that should not be retried at all:</p>
<pre><code class="language-python">import http.client
import json
import ssl
import urllib.error
import urllib.request

class ApiError(Exception):
    def __init__(self, status: int, retry_after: float | None = None, detail: str = ""):
        super().__init__(f"HTTP {status} {detail}".strip())
        self.status, self.retry_after = status, retry_after

def retry_after_seconds(headers) -&gt; float | None:
    """Server hint in seconds: retry-after-ms (OpenAI) or retry-after; None if absent or a date."""
    for name, scale in (("retry-after-ms", 1000.0), ("retry-after", 1.0)):
        try:
            return max(0.0, float(headers.get(name)) / scale)
        except (TypeError, ValueError):
            continue
    return None

def post_json(url: str, payload: dict, headers: dict, timeout: float = 120) -&gt; dict:
    req = urllib.request.Request(
        url, data=json.dumps(payload).encode(),
        headers={"content-type": "application/json", **headers})
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:  # HTTPError first: it is a URLError too
        detail = e.read(500).decode("utf-8", "replace")  # the provider's error type and message
        raise ApiError(e.code, retry_after_seconds(e.headers), detail) from e
    except urllib.error.URLError as e:
        if isinstance(e.reason, ssl.SSLCertVerificationError):
            raise  # certificate problems are permanent
        raise ConnectionError(str(e.reason)) from e
    except http.client.IncompleteRead as e:  # connection dropped mid-body
        raise ConnectionError("incomplete response") from e
</code></pre>
<p>Always pass a <code>timeout</code>: without one, a stalled connection can hang a worker indefinitely. Note that urllib’s timeout applies to each blocking socket operation, not the whole request, and that a long, non-streaming generation can legitimately take a minute or more. Set it above your slowest normal response, or a retry pays for the same generation twice. To size your rate limiter from real data, also log the remaining-quota headers providers send with every response (<code>r.headers</code> here).</p>
<h2>Exponential backoff with jitter and Retry-After</h2>
<p>After each failure, wait longer before the next attempt: 1 s, 2 s, 4 s, 8 s, up to a cap. Then pick a <em>random</em> wait between zero and that value (“full jitter”). Without the randomness, every worker that failed together retries together, and the server sees the same spike again. When the server sends a retry hint, wait at least that long, plus a little jitter. Cap the number of attempts, and stop starting new attempts after a deadline that is well above the request timeout, so a dead endpoint fails clearly without cutting off a slow but working one:</p>
<pre><code class="language-python">import asyncio
import random
import time

RETRYABLE = {408, 409, 429, 500, 502, 503, 504, 529}

async def call_with_retries(send, limiter, *, attempts=5, base=1.0, cap=30.0,
                            deadline=400.0, log=print):
    """send() makes one blocking request; limiter paces every attempt."""
    start = time.monotonic()
    for attempt in range(1, attempts + 1):
        await limiter.acquire()  # retries count against the provider's limit too
        try:
            return await asyncio.to_thread(send)
        except (ApiError, TimeoutError, ConnectionError) as e:
            status = getattr(e, "status", None)  # None = timeout or connection error
            if (status is not None and status not in RETRYABLE) or attempt == attempts:
                raise
            hint = getattr(e, "retry_after", None)
            backoff = random.uniform(0, min(cap, base * 2 ** (attempt - 1)))
            wait = hint + random.uniform(0, 1) if hint is not None else backoff
            if time.monotonic() - start + wait &gt; deadline:
                raise  # no new attempt would start before the deadline
            log(f"attempt {attempt} failed ({e}); retrying in {wait:.1f}s")
            await asyncio.sleep(wait)
</code></pre>
<h2>A client-side rate limiter</h2>
<p>Retries handle the 429s you get; a rate limiter prevents most of them. A token bucket refills at a steady rate and allows short bursts up to its capacity. Keep the burst small: a provider may enforce a per-minute limit over shorter intervals, so 45 requests in the first second can be rejected even under a 50-per-minute limit.</p>
<pre><code class="language-python">class RateLimiter:
    """About `rate` units per `per` seconds; bursts up to `burst` units."""

    def __init__(self, rate: float, per: float = 60.0, burst: float | None = None):
        self.fill = rate / per  # units added per second
        self.capacity = burst if burst is not None else max(1.0, rate / 10)
        self.tokens = self.capacity
        self.updated = time.monotonic()
        self.lock = asyncio.Lock()

    async def acquire(self, n: float = 1.0) -&gt; None:
        if n &gt; self.capacity:
            raise ValueError(f"request of {n} is larger than the bucket ({self.capacity})")
        async with self.lock:  # waiters are served in order
            while True:
                now = time.monotonic()
                self.tokens = min(self.capacity, self.tokens + (now - self.updated) * self.fill)
                self.updated = now
                if self.tokens &gt;= n:
                    self.tokens -= n
                    return
                await asyncio.sleep((n - self.tokens) / self.fill)
</code></pre>
<ul>
<li><strong>Tokens per minute:</strong> many providers also limit tokens, and some count input and output tokens separately. Make a bucket per limit, sized in tokens (with a burst at least as large as your biggest request), and call <code>await tpm.acquire(estimate)</code>. A rough estimate is prompt characters divided by four plus the maximum output tokens; it undercounts code and non-English text, so leave a margin.</li>
<li><strong>Per process:</strong> this limiter only sees its own process. If several workers share one API key, give each a fraction of the limit or use a shared limiter, for example in Redis.</li>
</ul>
<h2>Idempotency keys: never retry a side effect blindly</h2>
<p>A plain model call has no side effects (unless you give it server-side tools that act for you), so repeating one only costs time and tokens. Tool calls that change the world are different: sending an email, charging a card, writing a row. If the request timed out, you do not know whether it happened. Create an <strong>idempotency key</strong> once per logical operation (for example, the run id plus the step id), reuse it on every retry of that operation, and claim it before acting:</p>
<pre><code class="language-python">import sqlite3

# One connection shared by worker threads; use your real database in production.
db = sqlite3.connect("actions.db", isolation_level=None, check_same_thread=False)
db.execute("CREATE TABLE IF NOT EXISTS actions (key TEXT PRIMARY KEY, state TEXT, result TEXT)")

def run_once(key: str, fn, **args):
    """key = one logical operation, e.g. f"{run_id}:{step_id}", the same on every retry."""
    claimed = db.execute("INSERT OR IGNORE INTO actions VALUES (?, 'pending', NULL)", (key,)).rowcount == 1
    if not claimed:
        state, result = db.execute("SELECT state, result FROM actions WHERE key = ?", (key,)).fetchone()
        if state == "done":
            return json.loads(result)  # the original result, not a second action
        # 'pending': an earlier attempt may or may not have happened. Ask the receiving
        # system, or resend with the same idempotency key if it deduplicates; never just repeat.
        raise RuntimeError(f"{key} is pending: check before acting again")
    result = fn(**args)
    db.execute("UPDATE actions SET state = 'done', result = ? WHERE key = ?", (json.dumps(result), key))
    return result
</code></pre>
<p>The insert is atomic, so two workers can never both claim the same key. Where the receiving API supports an idempotency key header, send the same key there too, so a retried request is recognised on their side as well.</p>
<h2>Putting it together: bounded concurrency</h2>
<pre><code class="language-python">import os

async def ask(prompt: str, limiter: RateLimiter, slots: asyncio.Semaphore) -&gt; dict:
    payload = {"model": "your-model", "max_tokens": 800,
               "messages": [{"role": "user", "content": prompt}]}
    # Anthropic shown. OpenAI uses "authorization: Bearer &lt;key&gt;" and /v1/chat/completions,
    # and its payload and response shapes differ too: see its API reference.
    headers = {"x-api-key": os.environ["ANTHROPIC_API_KEY"], "anthropic-version": "2023-06-01"}
    async with slots:
        return await call_with_retries(
            lambda: post_json("https://api.anthropic.com/v1/messages", payload, headers),
            limiter)

async def main(prompts: list[str]) -&gt; list:
    limiter = RateLimiter(rate=45, per=60, burst=5)  # under a 50 requests/minute limit
    slots = asyncio.Semaphore(4)                    # at most 4 requests in flight
    return await asyncio.gather(*(ask(p, limiter, slots) for p in prompts),
                                return_exceptions=True)

if __name__ == "__main__":
    for r in asyncio.run(main(["First question", "Second question"])):
        print("failed:" if isinstance(r, Exception) else "ok:", r)
</code></pre>
<p>The semaphore caps how many calls run at once, and the limiter caps how many start per minute. A call that is waiting to retry keeps its slot, so heavy throttling slows the run down instead of piling on more requests. <code>return_exceptions=True</code> returns each failure as an exception in the results list, so one failed prompt does not cost you the answers to the others. Record each retry in your run trace; a rising retry count is often the first sign of a limit you are about to outgrow. <a href="https://learn.homingo.co.in/guides/llm-agent-evals-traces-budgets-python#traces">Tracing every step of an LLM agent</a> covers the trace format.</p>
<p>These pieces sit underneath the tool layer of our <a href="https://learn.homingo.co.in/guides/build-multi-agent-research-system-python#tools">multi-agent research system guide</a>, which adds argument checks, a shared cache and per-agent call caps.</p>
<blockquote>
<p><strong>Multi-agent systems: build an AI research team</strong>: Six modules, each with a short video lesson, a quiz check and a hands-on coding round that adds a tested piece of the team to your own repo. Together they cover the planner contract, handoffs, the tool layer, shared memory, the source checker, budgets, traces and an eval gate. The capstone is your team running live on a topic of your choice, with its reports, eval scores and code in a public GitHub repo. For builders who know Python and have called a model API. Certificate on completion. <a href="https://learn.homingo.co.in/courses/multi-agent-research-team?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=multiagent-course">See the course</a></p>
</blockquote>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the course linked above. Everything in it works without it.</em></p>
]]></content:encoded></item><item><title><![CDATA[How to build a multi-agent research system in Python: planner, researchers, source checker]]></title><description><![CDATA[Design a multi-agent research team in Python: a planner contract, parallel researchers, scoped handoffs, a shared tool layer, cited memory and a source…]]></description><link>https://homingolearn.hashnode.dev/how-to-build-a-multi-agent-research-system-in-python-planner-researchers-source-checker</link><guid isPermaLink="true">https://homingolearn.hashnode.dev/how-to-build-a-multi-agent-research-system-in-python-planner-researchers-source-checker</guid><category><![CDATA[llm]]></category><category><![CDATA[Python]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[multi-agent systems]]></category><dc:creator><![CDATA[Homingo Learn]]></dc:creator><pubDate>Mon, 02 Nov 2026 12:12:37 GMT</pubDate><content:encoded><![CDATA[<p><em>Originally published on</em> <a href="https://learn.homingo.co.in/guides/build-multi-agent-research-system-python"><em>Homingo Learn</em></a><em>.</em></p>
<p><em>The design behind an agent team that writes sourced reports: what to split, how agents pass work along, and how every sentence gets checked.</em></p>
<p>A single agent with a search tool can answer a question. Ask it for a sourced report on a broad topic and it runs out of context, repeats searches and cites pages that never said what it claims. A <strong>multi-agent system</strong> fixes this by splitting the work: a planner breaks the topic into sub-questions, researchers work on them in parallel, a writer drafts from what they found, and a source checker tests every sentence before the report ships. This guide walks through that design in plain Python (3.9 or newer), with the contracts that keep it from falling apart.</p>
<h2>When to split into agents (and when not to)</h2>
<ul>
<li><strong>Split</strong> when the work has independent parts that can run in parallel, when one context window cannot hold all the material, or when a result needs an independent check (a reviewer should not be the author).</li>
<li><strong>Do not split</strong> when the steps are strictly sequential and each needs the full previous output, or when the task fits comfortably in one call. Every handoff adds cost and a place to lose information.</li>
</ul>
<p>Research reports pass the test: sub-questions are independent, sources are many, and claims need checking by someone other than the writer.</p>
<h2>1. The planner returns a contract, not prose</h2>
<p>Ask the planner for JSON in a fixed shape and validate it in code. Duplicates are dropped, the number of tasks is capped, and a bad reply gets the exact error back for one retry:</p>
<pre><code class="language-python">import json
from dataclasses import dataclass

class PlanError(ValueError):
    pass

@dataclass
class Task:
    id: str
    question: str

def parse_plan(reply: str, max_tasks: int = 6) -&gt; list[Task]:
    try:
        data = json.loads(reply)
    except json.JSONDecodeError as e:
        raise PlanError(f"reply is not JSON: {e}") from e
    questions = data.get("sub_questions") if isinstance(data, dict) else None
    if not isinstance(questions, list) or not questions:
        raise PlanError('expected {"sub_questions": ["...", ...]} with at least one item')
    seen, tasks = set(), []
    for q in questions:
        if not isinstance(q, str) or not q.strip():
            raise PlanError("every sub-question must be a non-empty string")
        key = " ".join(q.lower().split())
        if key in seen:
            continue  # same question twice: keep one
        seen.add(key)
        tasks.append(Task(id=f"T{len(tasks) + 1}", question=q.strip()))
    if len(tasks) &gt; max_tasks:
        raise PlanError(f"at most {max_tasks} sub-questions, got {len(tasks)}")
    return tasks

PLANNER_PROMPT = """Split this research topic into 3 to 6 distinct sub-questions that can be
researched independently. Answer with JSON only: {{"sub_questions": ["...", "..."]}}
Topic: {topic}"""  # doubled braces are literal braces for str.format

def plan(topic: str, call_model) -&gt; list[Task]:
    prompt = PLANNER_PROMPT.format(topic=topic)
    reply = call_model(prompt)
    try:
        return parse_plan(reply)
    except PlanError as e:  # one retry, with the exact reason
        return parse_plan(call_model(f"{prompt}\n\nYour last reply was rejected: {e}. Answer again."))
</code></pre>
<h2>2. Handoffs: scoped context, validated returns, isolated failures</h2>
<p>Each researcher gets an envelope with only what it needs: its own question, the topic and the rules for its answer, not the whole conversation. Its reply is checked against a return contract (for example, a list of findings, each with a URL and an exact quote). Inside each researcher, an invalid reply or a transient error gets one retry with the reason as feedback. And one failing researcher must never stop the run:</p>
<pre><code class="language-python">import asyncio

async def run_researchers(tasks, research):
    """research(task) -&gt; validated findings; may raise."""
    results = await asyncio.gather(*(research(t) for t in tasks), return_exceptions=True)
    done, failed = [], []
    for task, result in zip(tasks, results):
        (failed if isinstance(result, BaseException) else done).append((task, result))
    return done, failed  # failures are reported in the run trace, never fatal

# Tool calls are blocking HTTP: inside research(), call them with
#   await asyncio.to_thread(toolbox.call, agent, "wikipedia_search", query=q)
# so the researchers really run at the same time.
</code></pre>
<h2>3. One door for every tool call</h2>
<p>Researchers call real search tools; the Wikipedia and arXiv APIs are public and a good place to start. Route every call through one object that checks the arguments, shares a cache across the team, caps real calls per agent, and turns errors into observations the agent can act on instead of crashes:</p>
<pre><code class="language-python">import json
import urllib.parse
import urllib.request

def wikipedia_search(query: str, limit: int = 5) -&gt; list[dict]:
    url = "https://en.wikipedia.org/w/api.php?" + urllib.parse.urlencode(
        {"action": "query", "list": "search", "srsearch": query, "srlimit": limit, "format": "json"})
    req = urllib.request.Request(url, headers={"User-Agent": "agent-team-tutorial/0.1 (you@example.com)"})
    with urllib.request.urlopen(req, timeout=15) as r:
        hits = json.load(r)["query"]["search"]
    return [{"title": h["title"],
             "url": "https://en.wikipedia.org/wiki/" + urllib.parse.quote(h["title"].replace(" ", "_"))}
            for h in hits]

TOOLS = {"wikipedia_search": {"required": ["query"], "fn": wikipedia_search}}

class ToolBox:
    def __init__(self, tools: dict, cap_per_agent: int = 8):
        self.tools, self.cap, self.cache, self.calls = tools, cap_per_agent, {}, {}

    def call(self, agent: str, name: str, **args):
        spec = self.tools.get(name)
        if spec is None:
            return {"error": f"unknown tool {name}"}
        missing = [k for k in spec["required"] if k not in args]
        if missing:
            return {"error": f"missing arguments: {missing}"}
        try:
            key = (name, json.dumps(args, sort_keys=True))
        except TypeError as e:
            return {"error": f"arguments must be JSON values: {e}"}
        if key in self.cache:  # shared by the team; cached answers do not count against the cap
            return self.cache[key]
        if self.calls.get(agent, 0) &gt;= self.cap:
            return {"error": f"call cap of {self.cap} reached for {agent}"}
        self.calls[agent] = self.calls.get(agent, 0) + 1
        try:
            result = spec["fn"](**args)
        except Exception as e:  # the agent sees the error and can try something else
            return {"error": f"{name} failed: {e}"}
        self.cache[key] = result
        return result
</code></pre>
<h2>4. Shared memory: one source of truth</h2>
<p>Findings from all researchers land in one memory. Every page gets one canonical URL and one stable id (<code>[S1]</code>, <code>[S2]</code> …), so the same page found twice is cited once. A note is kept only if its quote really appears on the fetched page:</p>
<pre><code class="language-python">from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

def canonical_url(url: str) -&gt; str:
    p = urlsplit(url.strip())
    host = p.netloc.lower().removeprefix("www.")
    query = urlencode([(k, v) for k, v in parse_qsl(p.query) if not k.startswith("utm_")])
    return urlunsplit(("https", host, p.path.rstrip("/") or "/", query, ""))

class Memory:
    def __init__(self):
        self.ids: dict[str, str] = {}
        self.notes: list[dict] = []

    def source_id(self, url: str) -&gt; str:
        key = canonical_url(url)
        if key not in self.ids:
            self.ids[key] = f"S{len(self.ids) + 1}"
        return self.ids[key]

    def add(self, task_id: str, url: str, claim: str, quote: str, page_text: str):
        q = " ".join(quote.split())
        if len(q) &lt; 20 or q not in " ".join(page_text.split()):
            return None  # no real quote, or not on the page: drop the note
        note = {"task": task_id, "source": self.source_id(url), "claim": claim, "quote": quote}
        if note not in self.notes:
            self.notes.append(note)
        return note["source"]
</code></pre>
<h2>5. The writer and the source checker</h2>
<p>The writer drafts from the memory only and cites ids like <code>[S3]</code>, one id per bracket (<code>[S1][S3]</code> for two). Then the source checker reads the draft sentence by sentence and puts each into one of four classes: <em>ok</em> (cited, and the cited quotes support it), <em>uncited</em>, <em>unknown_id</em> (cites an id that is not in memory) or <em>unsupported</em> (the quotes do not back it). The checker’s list drives one revision; after it, every sentence that is still not <em>ok</em> is removed.</p>
<pre><code class="language-python">import re

CITE = re.compile(r"\[(S\d+)\]")

def check_sentence(sentence: str, memory: Memory, supports) -&gt; str:
    """supports(sentence, quotes) -&gt; bool is a separate model call acting as judge."""
    ids = CITE.findall(sentence)
    if not ids:
        return "uncited"
    if any(i not in memory.ids.values() for i in ids):
        return "unknown_id"
    quotes = [n["quote"] for n in memory.notes if n["source"] in ids]
    return "ok" if supports(sentence, quotes) else "unsupported"
</code></pre>
<h2>6. Run it like production: budgets, traces, evals</h2>
<ul>
<li><strong>Budgets:</strong> a total token cap for the run, a cap per agent and a cap on tool calls, checked before every model call, so a looping agent stops instead of spending.</li>
<li><strong>Traces:</strong> one JSON line per step (agent, task, tool, tokens, time, result class). When a report is wrong, the trace shows which agent went wrong.</li>
<li><strong>Evals:</strong> a fixed set of topics and a score (for example, the share of sentences the checker marks ok), run in CI so a prompt change that makes reports worse fails the build.</li>
</ul>
<blockquote>
<p><strong>Multi-agent systems: build an AI research team</strong>: Six modules, each with a short video lesson, a quiz check and a hands-on coding round that adds a tested piece of the team to your own repo. Together they cover the planner contract, handoffs, the tool layer, shared memory, the source checker, budgets, traces and an eval gate. The capstone is your team running live on a topic of your choice, with its reports, eval scores and code in a public GitHub repo. For builders who know Python and have called a model API. Certificate on completion. <a href="https://learn.homingo.co.in/courses/multi-agent-research-team?utm_source=hashnode&amp;utm_medium=article&amp;utm_campaign=multiagent-course">See the course</a></p>
</blockquote>
<p>Building with Claude Code too? Our <a href="https://learn.homingo.co.in/guides/claude-code-skills-for-video">guide to Claude Code skills</a> shows how to package a workflow so the agent follows it every time.</p>
<hr />
<p><em>Disclosure: this guide is by Homingo, the team behind the course linked above. Everything in it works without it.</em></p>
]]></content:encoded></item></channel></rss>