<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: VincentChabran</title>
    <description>The latest articles on DEV Community by VincentChabran (@vincentchabran).</description>
    <link>https://dev.to/vincentchabran</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4018187%2F4eaa2608-d665-4c4b-ae95-f1a1889ca318.jpg</url>
      <title>DEV Community: VincentChabran</title>
      <link>https://dev.to/vincentchabran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vincentchabran"/>
    <language>en</language>
    <item>
      <title>Why Your Local Business Loses Leads Before You Even Open Your Email</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Mon, 03 Aug 2026 08:22:29 +0000</pubDate>
      <link>https://dev.to/vincentchabran/why-your-local-business-loses-leads-before-you-even-open-your-email-4328</link>
      <guid>https://dev.to/vincentchabran/why-your-local-business-loses-leads-before-you-even-open-your-email-4328</guid>
      <description>&lt;p&gt;&lt;em&gt;Written with AI assistance.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You run a local service business — a plumber, a gym, a salon, a small agency. Someone fills out your contact form at 9:47pm on a Tuesday. They're comparing you against two other providers in town, probably filling out the same form on their sites too. Whoever replies first, or at least acknowledges the request first, has a real edge — not because of superior sales skill, but because the requester's attention and urgency are highest right when they hit submit, and they decay from there.&lt;/p&gt;

&lt;p&gt;Here's the problem: most local business owners don't see that form submission until they check their email the next morning, or whenever they get a spare minute between jobs. By then, the requester has often already booked with whoever answered first, or simply moved on. The lead isn't lost because the business did a bad job — it's lost because nobody was watching the inbox in the fifteen minutes that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix isn't "answer faster." It's "acknowledge instantly."
&lt;/h2&gt;

&lt;p&gt;You can't have someone standing by the inbox 24/7 — that's not realistic for a solo plumber or a two-person salon. But you don't need a human to respond in the first sixty seconds. You need three things to happen automatically the moment a lead comes in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The lead gets an instant, personal-feeling acknowledgment&lt;/strong&gt; — "Got your request, we'll call you back shortly" — so they stop shopping around and stop assuming they've been ignored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lead lands in a system you can act on&lt;/strong&gt; — not buried in an inbox, but as a row in a sheet you can scan and prioritize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The owner gets pinged immediately&lt;/strong&gt;, with everything needed to close the loop fast — not a bare notification, but one with a direct way to call or reply right there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the entire shape of the problem. No CRM migration, no new phone system, no app to install on your technicians' phones. Just: form submitted → three things happen in the background, instantly, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that looks like as a workflow
&lt;/h2&gt;

&lt;p&gt;Structurally, this is a small automation: a form submission triggers a short chain — send the acknowledgment email, write a row to a tracking sheet, and notify the owner with the lead's details plus quick links to call or reply. Six steps, no manual handoff, no missed lead sitting in a spam folder until Thursday. The acknowledgment email and the tracking row happen in parallel with the owner alert, so nothing waits on anything else.&lt;/p&gt;

&lt;p&gt;The part that actually moves the needle isn't clever AI or personalization — it's the &lt;em&gt;instantness&lt;/em&gt;. A form that used to sit unanswered for hours now gets a reply inside seconds, and the owner gets the information needed to make a real callback while the lead still remembers filling out the form.&lt;/p&gt;

&lt;p&gt;If you're technical and already run n8n (or a similar automation tool) for your own business or for clients, this is a genuinely small build — a form trigger, a couple of email/notification nodes, and a spreadsheet write. If you'd rather not build and test it yourself, or you want something you can hand to a client tomorrow with install guides, onboarding checklists, and editable templates already done, that's exactly what we built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to get it
&lt;/h2&gt;

&lt;p&gt;We packaged this exact workflow — a ready-to-import, 6-node n8n Lead Capture workflow (English + French editions), plus a companion Google Review Automation workflow that closes the other end of the loop (asking happy customers for a review after the job's done, with duplicate-detection and a single smart reminder) — into the &lt;strong&gt;n8n Local Business Automation Kit&lt;/strong&gt;, built by &lt;strong&gt;Vixelys&lt;/strong&gt;, a French app &amp;amp; automation studio. It ships with install guides, an onboarding checklist, a QA checklist, editable email templates in both languages, and a commercial license that covers unlimited client projects.&lt;/p&gt;

&lt;p&gt;$69, instant download, 14-day guarantee if a kit doesn't import or work as described on a supported n8n version: &lt;strong&gt;&lt;a href="https://vincentdu2a.gumroad.com/l/mwukc" rel="noopener noreferrer"&gt;https://vincentdu2a.gumroad.com/l/mwukc&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>smallbusiness</category>
      <category>n8n</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>Your CLAUDE.md is probably too long — here's how to tell and what to do about it</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Mon, 03 Aug 2026 03:59:30 +0000</pubDate>
      <link>https://dev.to/vincentchabran/your-claudemd-is-probably-too-long-heres-how-to-tell-and-what-to-do-about-it-59b5</link>
      <guid>https://dev.to/vincentchabran/your-claudemd-is-probably-too-long-heres-how-to-tell-and-what-to-do-about-it-59b5</guid>
      <description>&lt;p&gt;Most &lt;code&gt;CLAUDE.md&lt;/code&gt; files start the same way: five or six bullet points written in the first afternoon of a project — naming convention, package manager, "don't run migrations automatically." Six months later the same file is 300+ lines: half-finished notes from a refactor, a paragraph explaining a decision that got reversed, three slightly different phrasings of "use TypeScript strict mode" added by three different people who didn't check if it was already there. Nobody deletes anything, because nobody's sure what's still load-bearing.&lt;/p&gt;

&lt;p&gt;The file still gets read in full on every single turn. That's the part people don't think through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Length isn't the real problem — signal-to-noise is
&lt;/h2&gt;

&lt;p&gt;A 300-line CLAUDE.md isn't bad because it's long. It's bad because a one-line rule that actually matters ("never touch &lt;code&gt;billing.py&lt;/code&gt;, it's synced from another repo") sits at the same visual and semantic weight as forty lines of restated formatting preferences and a stale note about a library you migrated off of two quarters ago. The model has no reliable way to tell which of those lines should change its next action and which are just historical residue. Everything reads as equally "instructions," so nothing gets the emphasis it needs.&lt;/p&gt;

&lt;p&gt;This is different from an instruction simply being ignored in a long session — it's noise actively competing with signal for the same attention budget, at every single turn, for the life of the project. A shorter file with the same important rule in it is &lt;em&gt;more&lt;/em&gt; likely to get followed, not less, purely because there's less competing for the same slot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick audit: read your CLAUDE.md and cut the dead weight
&lt;/h2&gt;

&lt;p&gt;Go through your file line by line and ask, for each one: &lt;em&gt;if I deleted this right now, would anything I do in the next hour actually change?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Common categories that fail that test and can usually go straight to the trash:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explanations of &lt;em&gt;why&lt;/em&gt; a convention exists, when the convention itself is one line. Keep the rule, cut the essay.&lt;/li&gt;
&lt;li&gt;Instructions for a tool, framework, or file that no longer exists in the repo.&lt;/li&gt;
&lt;li&gt;Anything phrased as "please try to" or "remember to" for something that must happen every time without exception — that's not a CLAUDE.md line at all, it's a hook (a shell command wired to a lifecycle event, so it's not competing for attention because it isn't a request in the first place).&lt;/li&gt;
&lt;li&gt;Multi-step procedures you only run occasionally ("when doing a release, do A then B then C") — that belongs in a slash command you invoke on demand, not a standing instruction read on every unrelated turn.&lt;/li&gt;
&lt;li&gt;Duplicate phrasings of the same rule added at different times. Keep the clearest one, delete the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's actually worth keeping
&lt;/h2&gt;

&lt;p&gt;What survives the cut tends to fall into a small set of categories: the shape of the project (where things live, what's generated vs. hand-written), naming and style conventions that aren't enforced by a linter, hard boundaries ("never," "always," specific files or directories that are off-limits), and non-obvious context a new contributor wouldn't guess — the kind of thing you'd tell a new hire in their first ten minutes, not their first week.&lt;/p&gt;

&lt;p&gt;If it's procedural and deterministic, it's a hook. If it's a multi-step task you trigger on purpose, it's a command. If it's a standing fact about the project that shapes every decision, it's CLAUDE.md. Most bloat comes from putting the first two kinds of information into the third bucket because it's the easiest file to open and add a line to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prune on a schedule, not just when it hurts
&lt;/h2&gt;

&lt;p&gt;The honest fix isn't a one-time cleanup — it's a habit. Every time you're tempted to add a line, check if an existing line already covers it. Every few weeks, re-read the whole file out loud (or have Claude summarize it back to you — if the summary surprises you, the file has drifted from what you think it says). A CLAUDE.md that's actively maintained at 40 tight lines beats one that grew to 400 and hasn't been fully read by a human in months.&lt;/p&gt;




&lt;p&gt;A starter CLAUDE.md template with commented placeholder sections (so you begin from a short, deliberate structure instead of an empty file that grows unchecked), plus 12 slash commands, 6 subagents, 5 hooks, and a git-aware statusline, is packaged as the &lt;strong&gt;Claude Code Power Pack&lt;/strong&gt; (copy-paste into &lt;code&gt;.claude/&lt;/code&gt;, €29): &lt;a href="https://vincentdu2a.gumroad.com/l/bqndfi" rel="noopener noreferrer"&gt;https://vincentdu2a.gumroad.com/l/bqndfi&lt;/a&gt;. It's an independent, unofficial project — not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and "Claude Code" are trademarks of Anthropic.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Written with AI assistance.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>Claude Code hooks: why "just tell it not to" doesn't hold up</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Sun, 02 Aug 2026 23:45:52 +0000</pubDate>
      <link>https://dev.to/vincentchabran/claude-code-hooks-why-just-tell-it-not-to-doesnt-hold-up-4d9b</link>
      <guid>https://dev.to/vincentchabran/claude-code-hooks-why-just-tell-it-not-to-doesnt-hold-up-4d9b</guid>
      <description>&lt;p&gt;If you've spent time customizing Claude Code, you've probably written a line like this in your &lt;code&gt;CLAUDE.md&lt;/code&gt;: "always run the formatter after editing a file" or "never touch &lt;code&gt;.env&lt;/code&gt; or any &lt;code&gt;.pem&lt;/code&gt;/&lt;code&gt;.key&lt;/code&gt; file." It works — until a long session, a big diff, or a context window full of other instructions makes that one line easy to deprioritize. The model isn't being careless; a system prompt is advisory. Every instruction in it competes with everything else in context for attention, and instructions written early tend to fade as the conversation grows.&lt;/p&gt;

&lt;p&gt;That's a structural limit, not a prompting mistake. And it means anything you actually need to happen every single time — not "usually," not "when the model remembers to" — doesn't belong in a prompt at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a hook actually is
&lt;/h2&gt;

&lt;p&gt;Claude Code hooks are shell commands wired to specific points in the tool's lifecycle: after a file edit, before a tool runs, when Claude finishes responding, when it needs your permission, at the start of a session. They execute as code, deterministically, regardless of what's in the conversation or what the model "decided" to prioritize that turn. The model doesn't get a vote — the hook just runs.&lt;/p&gt;

&lt;p&gt;That distinction matters more than it sounds. A CLAUDE.md rule is a request you're hoping gets honored. A hook is a guarantee, because it's not the model doing the enforcing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the line actually falls
&lt;/h2&gt;

&lt;p&gt;Not everything belongs in a hook. The useful split is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Must always happen, no judgment required → hook.&lt;/strong&gt; Reformatting a file after every edit. Blocking writes to a credentials file before they land. Running your test suite the moment Claude stops and printing a one-line pass/fail. Pinging your desktop when Claude is waiting on you. Logging that a session started, with a timestamp, every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Benefits from reasoning about the specific case → prompt / subagent.&lt;/strong&gt; Deciding &lt;em&gt;whether&lt;/em&gt; a diff has a security problem, &lt;em&gt;what&lt;/em&gt; a docstring should say, &lt;em&gt;how&lt;/em&gt; to restructure a function — that's judgment, and judgment is what the model is for. Trying to hardcode that into a shell script just moves the fragility somewhere worse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting this backwards is the common failure mode: people write increasingly elaborate CLAUDE.md instructions trying to force deterministic behavior out of a probabilistic system, when a five-line shell hook would have made the question moot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cheap companion: visibility instead of enforcement
&lt;/h2&gt;

&lt;p&gt;A statusline doesn't enforce anything — it's just a line under your prompt showing which model, which directory, which git branch you're currently in. It matters for a much smaller reason: once you're running more than one Claude Code session in parallel (a common outcome of subagents and worktrees), "which one is this again" becomes a real source of mistakes. Cheap to add, easy to forget you need until you're context-switching between four terminals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Before adding another paragraph to your CLAUDE.md, ask whether what you actually want is a rule the model should weigh, or a guarantee that shouldn't depend on it weighing anything at all. If it's the latter, write a hook instead.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Written with AI assistance.)&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A working set of these — auto-format on edit, a secret-file guard that blocks writes to &lt;code&gt;.env&lt;/code&gt;/&lt;code&gt;*.pem&lt;/code&gt;/&lt;code&gt;*.key&lt;/code&gt; before they happen, run-tests-on-stop, desktop notifications, and session logging — plus 12 slash commands, 6 subagents, and a git-aware statusline, is packaged as the &lt;strong&gt;Claude Code Power Pack&lt;/strong&gt; (copy-paste into &lt;code&gt;.claude/&lt;/code&gt;, €29): &lt;a href="https://vincentdu2a.gumroad.com/l/bqndfi" rel="noopener noreferrer"&gt;https://vincentdu2a.gumroad.com/l/bqndfi&lt;/a&gt;. It's an independent, unofficial project — not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and "Claude Code" are trademarks of Anthropic.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>Splitting one "review this code" subagent into three — what changes</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Sun, 02 Aug 2026 15:32:10 +0000</pubDate>
      <link>https://dev.to/vincentchabran/splitting-one-review-this-code-subagent-into-three-what-changes-lp1</link>
      <guid>https://dev.to/vincentchabran/splitting-one-review-this-code-subagent-into-three-what-changes-lp1</guid>
      <description>&lt;p&gt;A common way to set up Claude Code review is a single subagent with one prompt: "review this diff for bugs, missing tests, and security issues." One report at the end. It works, sort of — but the reports tend to be shallow in a specific, predictable way: whichever concern comes first in the prompt gets the most attention, and whatever comes last gets a token sentence or nothing at all.&lt;/p&gt;

&lt;p&gt;That's not a model-quality problem. It's a structural one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one agent doing three jobs underperforms
&lt;/h2&gt;

&lt;p&gt;A single subagent has one context window and one output budget for the task. Ask it to reason about logic correctness, test coverage, and security in the same pass, and it has to allocate attention across three different mental models at once: "does this code do what it claims," "what inputs are missing coverage," and "how could this be abused." Those aren't the same lens. A reviewer scanning for logic bugs is pattern-matching against intent. A security auditor is pattern-matching against attacker behavior. Forcing one pass to do both means it either picks a dominant lens and treats the others as an afterthought, or it spreads evenly and goes shallow everywhere.&lt;/p&gt;

&lt;p&gt;In practice, that tends to show up as: the review agent flags a naming inconsistency and an off-by-one, then adds a bullet like "consider adding tests" with no specifics, and rarely surfaces anything security-related unless the diff is obviously about auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting the concern, not just the prompt
&lt;/h2&gt;

&lt;p&gt;The fix isn't a better prompt — it's three separate subagents, each with a narrow job and only the tool access it actually needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;code-reviewer&lt;/strong&gt;: read-only access to the diff and surrounding files. Its entire prompt is about correctness, security, and design issues, severity-ranked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;test-writer&lt;/strong&gt;: read access, plus write access scoped to test files. Its job is to find code paths the diff touches that have no coverage and write deterministic tests in the framework already in the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;security-auditor&lt;/strong&gt;: read-only, but with permission to grep across the whole repo, not just the diff. Injection risks, unsafe deserialization, and secrets-in-code usually require seeing how a value flows outside the immediate change — a diff-only view misses that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one produces a short, focused report instead of one long generic one. Dispatched together (in parallel, since they don't depend on each other), the total review reads like three specialists each did a pass, because that's literally what happened.&lt;/p&gt;

&lt;p&gt;The difference isn't that any individual subagent is "smarter" than the combined one — it's that scoping the tool access and the prompt to a single concern removes the tradeoff between depth and breadth. The security-auditor isn't competing for attention with "is this variable named well." It only has one job, so it does that job instead of a third of three jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that's easy to get wrong
&lt;/h2&gt;

&lt;p&gt;The temptation is to give every subagent full repo access "just in case." Don't — it slows them down and, worse, it re-introduces the same attention dilution you were trying to fix, just at the tool-selection level instead of the reasoning level. Least-privilege tool access isn't just a security nicety here, it's what keeps each subagent's job narrow enough to actually be good at it.&lt;/p&gt;




&lt;p&gt;This reviewer/test-writer/security-auditor split, plus three more subagents (debugger, doc-writer, refactor-specialist), 12 slash commands, 5 hooks, a git-aware statusline, and a CLAUDE.md template are packaged into the &lt;strong&gt;Claude Code Power Pack&lt;/strong&gt; (copy-paste into &lt;code&gt;.claude/&lt;/code&gt;, €29): &lt;a href="https://vincentdu2a.gumroad.com/l/bqndfi" rel="noopener noreferrer"&gt;https://vincentdu2a.gumroad.com/l/bqndfi&lt;/a&gt;. It's an independent, unofficial project — not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and "Claude Code" are trademarks of Anthropic.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Written with AI assistance.)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>abotwrotethis</category>
    </item>
    <item>
      <title>I Automated the Awkward Part of Asking for Google Reviews (n8n, 14 nodes, no code)</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Fri, 31 Jul 2026 17:15:56 +0000</pubDate>
      <link>https://dev.to/vincentchabran/i-automated-the-awkward-part-of-asking-for-google-reviews-n8n-14-nodes-no-code-5gh6</link>
      <guid>https://dev.to/vincentchabran/i-automated-the-awkward-part-of-asking-for-google-reviews-n8n-14-nodes-no-code-5gh6</guid>
      <description>&lt;p&gt;Every local business owner I've talked to agrees on the same thing: asking a client for a Google review feels awkward. You either forget to do it, do it too soon (before they've had time to actually think about the job), or do it once and never follow up because chasing someone for a review feels pushier than the job itself.&lt;/p&gt;

&lt;p&gt;So I built a small n8n workflow that removes the awkward part and keeps the polite part.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;The workflow triggers when a job is marked complete (from a form, a CRM row, or a simple spreadsheet update — whatever the freelancer already uses). It then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks whether this contact has already been sent a review request before. If yes, it stops there and just logs it. No client gets asked twice for the same job.&lt;/li&gt;
&lt;li&gt;Sends a short email with a direct link to leave a Google review — not a generic "please review us," a link that drops the client straight into the review box.&lt;/li&gt;
&lt;li&gt;Waits until day 7.&lt;/li&gt;
&lt;li&gt;Checks again before doing anything else: has a review already come in, or did the contact opt out? If either is true, it stops. No reminder goes out to someone who already left a review or asked not to be contacted.&lt;/li&gt;
&lt;li&gt;If neither happened, it sends exactly one reminder email. Not two, not a drip sequence — one.&lt;/li&gt;
&lt;li&gt;Every outcome — sent, reviewed, reminded, skipped, opted out — gets logged to a Google Sheet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's 14 nodes total, and the two edge cases above (duplicate detection, stop-on-review) are the ones that actually matter. A review-request workflow that doesn't check for those two things is the kind of thing that annoys a client into leaving a &lt;em&gt;bad&lt;/em&gt; review instead of a good one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It doesn't guarantee you'll get more reviews. No workflow can promise that — it depends on the job you did and whether the client actually liked it. What it guarantees is that the ask happens consistently, at the right moment, without ever asking twice or nagging someone who already reviewed you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it as a kit instead of a one-off
&lt;/h2&gt;

&lt;p&gt;I do this kind of automation work for local businesses (plumbers, hairdressers, small studios). I got tired of rebuilding the same 14 nodes from scratch every time, so I packaged it: the importable n8n JSON, a setup guide in English and French, a one-page checklist for the end client, and a short compliance note on sending automated follow-up emails responsibly.&lt;/p&gt;

&lt;p&gt;I also put together a small &lt;a href="https://github.com/VincentChabran/n8n-google-review-automation-kit" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; that shows the structure of the flow (the node sequence, not the full working file — that part's in the kit).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to get it
&lt;/h2&gt;

&lt;p&gt;The kit — importable workflow + FR/EN guides + client checklist + compliance note — is on Gumroad for $49: &lt;a href="https://vincentdu2a.gumroad.com/l/lxaziv" rel="noopener noreferrer"&gt;https://vincentdu2a.gumroad.com/l/lxaziv&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the n8n setup in the comments if anyone's building something similar.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>nocode</category>
      <category>smallbusiness</category>
    </item>
    <item>
      <title>A Game Design Document template sized for solo devs (not AAA studios)</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Fri, 31 Jul 2026 08:59:27 +0000</pubDate>
      <link>https://dev.to/vincentchabran/a-game-design-document-template-sized-for-solo-devs-not-aaa-studios-3o2e</link>
      <guid>https://dev.to/vincentchabran/a-game-design-document-template-sized-for-solo-devs-not-aaa-studios-3o2e</guid>
      <description>&lt;p&gt;I've been making small games as a side project, and every time I start one, the same thing happens: the "design doc" is either a 40-page template I never finish, or a couple of scattered notes that don't survive contact with week 3.&lt;/p&gt;

&lt;p&gt;So I put together a one-document GDD template sized for solo and small-team projects -- the kind of thing you can actually fill in an afternoon, not a semester.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it covers
&lt;/h2&gt;

&lt;p&gt;11 sections: Overview, Elevator Pitch, Core Mechanics, Game Loop, Controls, Characters, Levels &amp;amp; Progression, Art &amp;amp; Audio Direction, Scope &amp;amp; Cut List, Roadmap &amp;amp; Milestones, Risks &amp;amp; Open Questions.&lt;/p&gt;

&lt;p&gt;The one I'd flag: &lt;strong&gt;Scope &amp;amp; Cut List&lt;/strong&gt;. It's the section most first-timers skip, and skipping it is exactly how a two-week prototype turns into a six-month grind. Writing down what you're deliberately NOT building is as useful as writing down what you are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example included
&lt;/h2&gt;

&lt;p&gt;Rather than ship an empty template and a wall of instructions, the pack includes a full worked example -- a tiny fictional arcade game called "Ember Drift" -- with every section filled in, so you can see what "done" looks like for a small project before you write your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format
&lt;/h2&gt;

&lt;p&gt;Markdown (.md) + PDF. Works in any text editor, or drop it into Notion, Obsidian, or Google Docs.&lt;/p&gt;

&lt;p&gt;Repo with the full section breakdown: &lt;a href="https://github.com/VincentChabran/game-design-document-template" rel="noopener noreferrer"&gt;https://github.com/VincentChabran/game-design-document-template&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full template + PDF + worked example: &lt;a href="https://vincentdu2a.gumroad.com/l/galfj" rel="noopener noreferrer"&gt;https://vincentdu2a.gumroad.com/l/galfj&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Built with AI assistance, reviewed and edited by hand before release -- disclosed on the product page.)&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>indiedev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>8 free-to-preview minimalist SVG weather icons (sun, cloud, rain, snow, wind, storm, fog, moon)</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Thu, 30 Jul 2026 20:27:44 +0000</pubDate>
      <link>https://dev.to/vincentchabran/8-free-to-preview-minimalist-svg-weather-icons-sun-cloud-rain-snow-wind-storm-fog-moon-46j9</link>
      <guid>https://dev.to/vincentchabran/8-free-to-preview-minimalist-svg-weather-icons-sun-cloud-rain-snow-wind-storm-fog-moon-46j9</guid>
      <description>&lt;p&gt;I put together a small set of 8 weather icons as SVG: sun, cloud, rain, snow, wind, storm, fog, moon. Flat, single-color shapes, built to stay legible at small sizes — the kind of thing you'd drop into a weather widget, a forecast app, a dashboard or a game HUD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SVG
&lt;/h2&gt;

&lt;p&gt;Scalable to any size without quality loss, and easy to recolor by editing the &lt;code&gt;fill&lt;/code&gt; attribute directly — no design software needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to look
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The 8 icons are viewable in a small GitHub repo: &lt;a href="https://github.com/VincentChabran/weather-icons-svg-pack" rel="noopener noreferrer"&gt;https://github.com/VincentChabran/weather-icons-svg-pack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The packaged version (zip + README, licensed for personal and commercial use) is on Gumroad: &lt;a href="https://vincentdu2a.gumroad.com/l/tfuwr" rel="noopener noreferrer"&gt;https://vincentdu2a.gumroad.com/l/tfuwr&lt;/a&gt; ($3)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A note on process
&lt;/h2&gt;

&lt;p&gt;The icon set was created with the assistance of generative AI tools as part of the design process, then reviewed and finalized by hand before release — a couple of the shapes went through multiple redraws to make sure each one was an original construction rather than a close copy of an existing icon set.&lt;/p&gt;

&lt;p&gt;If you use them in something, I'd be curious to see it.&lt;/p&gt;

</description>
      <category>svg</category>
      <category>webdev</category>
      <category>design</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why Claude Code keeps asking for permission — and how to fix your setup so it stops</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Wed, 08 Jul 2026 17:34:21 +0000</pubDate>
      <link>https://dev.to/vincentchabran/why-claude-code-keeps-asking-for-permission-and-how-to-fix-your-setup-so-it-stops-3ho8</link>
      <guid>https://dev.to/vincentchabran/why-claude-code-keeps-asking-for-permission-and-how-to-fix-your-setup-so-it-stops-3ho8</guid>
      <description>&lt;p&gt;You set up Claude Code, you're in flow, and then: &lt;em&gt;"Allow Bash(git diff)?"&lt;/em&gt; You click allow. Thirty seconds later: &lt;em&gt;"Allow Bash(git log)?"&lt;/em&gt; Allow. Then the same slash command you already approved yesterday asks &lt;strong&gt;again&lt;/strong&gt;. At some point you're tempted to reach for &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; and be done with it.&lt;/p&gt;

&lt;p&gt;Don't. Permission fatigue isn't fixed by surrendering — it's fixed by understanding the three specific reasons Claude Code prompts you, and configuring each one away. That takes about ten minutes, and this article is the complete walkthrough: the permission model, a documented trap inside slash commands that keeps prompting &lt;em&gt;even when you've allowed everything&lt;/em&gt;, a copy-paste &lt;code&gt;settings.json&lt;/code&gt;, and the case where a hook is safer than a broader allow.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/vincentchabran/the-claude-code-setup-i-install-in-every-repo-slash-commands-a-review-subagent-and-hooks-404j"&gt;a previous article&lt;/a&gt; I showed the slash commands, subagent, and hooks I install in every repo. This is the deep-dive on the part of that setup that generates the most questions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unofficial, community-made. Not affiliated with or endorsed by Anthropic.&lt;/strong&gt; "Claude" and "Claude Code" are trademarks of Anthropic. This is independent config &lt;em&gt;for&lt;/em&gt; Claude Code, not a product &lt;em&gt;of&lt;/em&gt; Anthropic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything below was verified by actually running it on &lt;strong&gt;Claude Code v2.1.x (July 2026)&lt;/strong&gt;. The tool ships fast — if you're reading this much later, double-check the details against the current docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three reasons you get prompted
&lt;/h2&gt;

&lt;p&gt;Every permission prompt you see is one of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No rule covers the tool call.&lt;/strong&gt; The default answer for anything not explicitly allowed is "ask." Fix: an allowlist in &lt;code&gt;settings.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A slash command injects shell output, and nothing covers it&lt;/strong&gt; — neither the command's &lt;code&gt;allowed-tools&lt;/code&gt; frontmatter nor your settings. Fix: list the commands in the frontmatter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The command &lt;em&gt;is&lt;/em&gt; listed, but Claude Code can't statically verify it&lt;/strong&gt; — because it contains dynamic constructs like &lt;code&gt;$(...)&lt;/code&gt;. This is the sneaky one, and it prompts on &lt;em&gt;every&lt;/em&gt; invocation no matter what you've approved. Fix: rewrite the command (section below).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's take them in order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason 1: the allowlist you haven't written yet
&lt;/h2&gt;

&lt;p&gt;Claude Code reads permission rules from three settings files, most specific wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.claude/settings.json&lt;/code&gt; — you, on every project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; — this project, committed and shared with your team.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/settings.local.json&lt;/code&gt; — this project, just you (git-ignored automatically).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rules live under a &lt;code&gt;permissions&lt;/code&gt; key in three lists: &lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;ask&lt;/code&gt;, and &lt;code&gt;deny&lt;/code&gt; — and &lt;strong&gt;deny always beats ask, which beats allow&lt;/strong&gt;. A rule is a tool name plus an optional specifier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Bash(git diff:*)&lt;/code&gt; — any command starting with &lt;code&gt;git diff&lt;/code&gt; (the &lt;code&gt;:*&lt;/code&gt; makes it a prefix match; without it, only the exact command matches).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Read(./.env)&lt;/code&gt; / &lt;code&gt;Read(./secrets/**)&lt;/code&gt; — gitignore-style paths for file tools.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WebFetch(domain:docs.anthropic.com)&lt;/code&gt; — fetches scoped to a domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two practical notes. First, when you click "always allow" on a prompt, Claude Code writes the rule to your &lt;strong&gt;local project settings&lt;/strong&gt; — handy, but it means your allowlist grows ad hoc and never transfers to the next repo. Writing it deliberately in &lt;code&gt;~/.claude/settings.json&lt;/code&gt; fixes the fatigue everywhere at once. Second, run &lt;code&gt;/permissions&lt;/code&gt; inside Claude Code any time to see exactly which rules are in effect and where each one comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason 2: slash commands have their own gate
&lt;/h2&gt;

&lt;p&gt;A custom slash command (a markdown file in &lt;code&gt;.claude/commands/&lt;/code&gt;) can inject live shell output into its prompt with &lt;code&gt;!`command`&lt;/code&gt;. That's the feature that makes commands like &lt;code&gt;/review&lt;/code&gt; work on your &lt;em&gt;actual&lt;/em&gt; diff. But those injected commands run the moment you invoke the slash command — so each one needs approval first. On v2.1.x, an injection is auto-approved if &lt;em&gt;any&lt;/em&gt; of three things covers it: the command's own &lt;code&gt;allowed-tools&lt;/code&gt; frontmatter, an &lt;code&gt;allow&lt;/code&gt; rule in your settings, or Claude Code's built-in pass for a few safe read-only commands (like &lt;code&gt;date&lt;/code&gt;). If none of the three covers it, you get prompted. The frontmatter is the one to reach for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;committing"&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash(git status:*), Bash(git diff:*)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="p"&gt;-&lt;/span&gt; Changed files: !&lt;span class="sb"&gt;`git status --short`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Full diff: !&lt;span class="sb"&gt;`git diff HEAD`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both injections are covered, so this runs silently — for everyone. Skip the frontmatter and the command may still &lt;em&gt;seem&lt;/em&gt; fine on your machine, if your own settings happen to allow &lt;code&gt;git status&lt;/code&gt; and &lt;code&gt;git diff&lt;/code&gt; — and then it prompts the teammate who cloned the repo without your allowlist. The frontmatter is what makes a slash command portable and self-sufficient: it carries its own permissions instead of borrowing yours. If a command of yours always asks, this is the first thing to check.&lt;/p&gt;

&lt;p&gt;So far, so documented. Now the trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reason 3: the "too complex to verify" trap
&lt;/h2&gt;

&lt;p&gt;Here's a command that looks perfectly configured — and prompts on &lt;strong&gt;every invocation anyway&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Summarize what this branch changes compared to main&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash(git log:*), Bash(git diff:*), Bash(git merge-base:*)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Commits on this branch: !&lt;span class="sb"&gt;`git log --oneline $(git merge-base main HEAD)..HEAD`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Files touched: !&lt;span class="sb"&gt;`git diff --stat $(git merge-base main HEAD)...HEAD`&lt;/span&gt;

&lt;span class="gu"&gt;## Task&lt;/span&gt;

Summarize this branch for a teammate: what changed, why, and what looks risky.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every piece is listed in &lt;code&gt;allowed-tools&lt;/code&gt; — &lt;code&gt;git log&lt;/code&gt;, &lt;code&gt;git diff&lt;/code&gt;, &lt;code&gt;git merge-base&lt;/code&gt;. And yet: permission prompt, each time.&lt;/p&gt;

&lt;p&gt;The reason: to auto-approve an injected command, Claude Code has to &lt;strong&gt;statically prove&lt;/strong&gt; it matches an allowed pattern. &lt;code&gt;git diff --stat HEAD&lt;/code&gt; is a literal string — provable. &lt;code&gt;git diff --stat $(git merge-base main HEAD)...HEAD&lt;/code&gt; contains a command substitution whose output can't be known before running it — so the analyzer classifies the whole command as &lt;strong&gt;too complex to verify&lt;/strong&gt; and falls back to asking you. In my testing on v2.1.x this applies to &lt;code&gt;$(...)&lt;/code&gt;, backticks, and even innocent-looking git expansions like &lt;code&gt;@{u}&lt;/code&gt; (&lt;code&gt;!`git log @{u}..HEAD`&lt;/code&gt; prompts too, despite containing no substitution at all).&lt;/p&gt;

&lt;p&gt;This is a security feature, not a bug — a &lt;code&gt;$(...)&lt;/code&gt; buried inside a pre-approved-looking command is exactly how a prompt-injection attack would smuggle something past your allowlist. So don't fight the analyzer. Restructure instead:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the injections static. Move the dynamic logic into the task, where Claude resolves it at runtime.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Summarize what this branch changes compared to main&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash(git branch:*), Bash(git log:*), Bash(git diff:*), Bash(git merge-base:*)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Current branch: !&lt;span class="sb"&gt;`git branch --show-current`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Recent commits (fallback view): !&lt;span class="sb"&gt;`git log --oneline -15`&lt;/span&gt;

&lt;span class="gu"&gt;## Task&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Find where this branch forked: run &lt;span class="sb"&gt;`git merge-base main HEAD`&lt;/span&gt;.
&lt;span class="p"&gt;2.&lt;/span&gt; Read the exact range using the real SHA from step 1: &lt;span class="sb"&gt;`git log &amp;lt;base&amp;gt;..HEAD --oneline`&lt;/span&gt;
   and &lt;span class="sb"&gt;`git diff --stat &amp;lt;base&amp;gt;...HEAD`&lt;/span&gt; (substitute the SHA for &lt;span class="sb"&gt;`&amp;lt;base&amp;gt;`&lt;/span&gt; — don't nest
   &lt;span class="sb"&gt;`$(git merge-base ...)`&lt;/span&gt; inside the command, or it will trigger a permission prompt).
&lt;span class="p"&gt;3.&lt;/span&gt; Summarize this branch for a teammate: what changed, why, and what looks risky.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same capability, zero prompts. The injections are now literal strings the analyzer can match. The fork-point lookup happens as a normal tool call during the task: Claude runs &lt;code&gt;git merge-base main HEAD&lt;/code&gt; (covered by &lt;code&gt;Bash(git merge-base:*)&lt;/code&gt;), reads the SHA, then runs &lt;code&gt;git log a1b2c3d..HEAD&lt;/code&gt; (covered by &lt;code&gt;Bash(git log:*)&lt;/code&gt;). Note the warning inside the prompt itself: the same rule applies at runtime, so telling Claude to substitute the literal SHA — instead of nesting &lt;code&gt;$(...)&lt;/code&gt; — is what keeps step 2 prompt-free.&lt;/p&gt;

&lt;p&gt;Rule of thumb: &lt;strong&gt;&lt;code&gt;!`...`&lt;/code&gt; injections should be commands you could type from muscle memory — fixed strings, no substitutions, no expansions.&lt;/strong&gt; Anything conditional belongs in the task instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A clean, copyable &lt;code&gt;settings.json&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here's a complete baseline that removes the everyday prompts without opening anything scary. It also includes an auto-format hook (from the free kit linked at the end) so you can see how &lt;code&gt;permissions&lt;/code&gt; and &lt;code&gt;hooks&lt;/code&gt; coexist in one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git status:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git diff:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git log:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git branch:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git show:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git add:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git commit:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git merge-base:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(ls:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(npm test:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(npm run lint:*)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./secrets/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push --force:*)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write|MultiEdit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f=$(jq -r '.tool_input.file_path // empty'); [ -z &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ] &amp;amp;&amp;amp; exit 0; case &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; in *.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.scss|*.html|*.md|*.yml|*.yaml) command -v prettier &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; prettier --write &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 ;; *.py) command -v black &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; black -q &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 ;; *.go) command -v gofmt &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; gofmt -w &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 ;; *.rs) command -v rustfmt &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; rustfmt &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 ;; esac; exit 0"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why these choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The allows are read-mostly.&lt;/strong&gt; Status, diffs, logs, tests, lint — the commands Claude runs constantly and that can't hurt you. &lt;code&gt;git add&lt;/code&gt; and &lt;code&gt;git commit&lt;/code&gt; are there because committing is the point; there's deliberately &lt;strong&gt;no &lt;code&gt;git push&lt;/code&gt;&lt;/strong&gt; in the allowlist, so anything leaving your machine still asks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The denies protect secrets from being &lt;em&gt;read&lt;/em&gt;,&lt;/strong&gt; not just edited. Claude never needs the contents of &lt;code&gt;.env&lt;/code&gt; to help you code.&lt;/li&gt;
&lt;li&gt;Put it in &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for yourself everywhere, or &lt;code&gt;.claude/settings.json&lt;/code&gt; to share the policy with your team. Restart Claude Code after saving.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a hook beats a broader allow
&lt;/h2&gt;

&lt;p&gt;Notice the deny rule &lt;code&gt;Bash(git push --force:*)&lt;/code&gt;. It works — but it's a &lt;strong&gt;string prefix&lt;/strong&gt;. &lt;code&gt;git push -f&lt;/code&gt; sails right past it. Permission rules are a convenience layer; when you want a &lt;em&gt;guarantee&lt;/em&gt;, use a &lt;code&gt;PreToolUse&lt;/code&gt; hook, which runs before the tool executes and can block it regardless of what the permission rules would have allowed.&lt;/p&gt;

&lt;p&gt;Say you've allowed file edits broadly because the edit prompts were driving you mad. This hook makes sure that no matter what, credential files stay untouched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write|MultiEdit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f=$(jq -r '.tool_input.file_path // empty'); case &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; in *.env|*.env.*|*.pem|*.key) echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Blocked: $f looks like a credentials file. Edit it yourself.&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;&amp;amp;2; exit 2 ;; esac; exit 0"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook reads the tool's JSON payload on stdin (&lt;code&gt;jq&lt;/code&gt; pulls out the file path — &lt;code&gt;brew install jq&lt;/code&gt; / &lt;code&gt;sudo apt install jq&lt;/code&gt;), and &lt;strong&gt;exit code 2 blocks the action&lt;/strong&gt; and hands your stderr message back to Claude as the reason, so it adapts instead of retrying blindly. Exit 0 stays out of the way.&lt;/p&gt;

&lt;p&gt;The mental model: &lt;strong&gt;allow rules decide what runs without asking; hooks decide what never runs at all.&lt;/strong&gt; Broad allow + narrow hard-block is a much nicer place to work than narrow allows + constant prompts — you get the flow back without giving up the guarantees you actually care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10-minute fix, recapped
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write a deliberate allowlist of read-mostly commands in &lt;code&gt;~/.claude/settings.json&lt;/code&gt; — stop accumulating one-off "always allow" clicks.&lt;/li&gt;
&lt;li&gt;Give every custom slash command an &lt;code&gt;allowed-tools&lt;/code&gt; frontmatter covering its &lt;code&gt;!`...`&lt;/code&gt; injections.&lt;/li&gt;
&lt;li&gt;Keep those injections &lt;strong&gt;static&lt;/strong&gt;. Move anything with &lt;code&gt;$(...)&lt;/code&gt;, backticks, or &lt;code&gt;@{u}&lt;/code&gt;-style expansions into the task instructions.&lt;/li&gt;
&lt;li&gt;Guard the truly untouchable (secrets, force-pushes) with a &lt;code&gt;PreToolUse&lt;/code&gt; hook exiting 2, not with an ever-longer prompt-clicking habit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Get the files
&lt;/h2&gt;

&lt;p&gt;The slash commands, the subagent, and the auto-format hook from this series — with the permission lessons above already applied — live in a free, MIT-licensed repo you can copy straight into &lt;code&gt;.claude/&lt;/code&gt; (the PreToolUse credentials guard isn't bundled with the kit — copy it straight from this article):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/VincentChabran/claude-code-starter-kit" rel="noopener noreferrer"&gt;Claude Code Starter Kit on GitHub&lt;/a&gt;&lt;/strong&gt; (MIT — use it, change it, ship it)&lt;/p&gt;

&lt;p&gt;And if you want the extended set (12 commands, 6 subagents, 5 hooks, a git-aware status line, and a step-by-step guide), that's the paid &lt;a href="https://vincentdu2a.gumroad.com/l/bqndfi" rel="noopener noreferrer"&gt;Power Pack&lt;/a&gt; — but everything in this article works with the free repo alone.&lt;/p&gt;

&lt;p&gt;Happy building.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Claude Code setup I install in every repo: slash commands, a review subagent, and hooks</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Tue, 07 Jul 2026 18:04:30 +0000</pubDate>
      <link>https://dev.to/vincentchabran/the-claude-code-setup-i-install-in-every-repo-slash-commands-a-review-subagent-and-hooks-404j</link>
      <guid>https://dev.to/vincentchabran/the-claude-code-setup-i-install-in-every-repo-slash-commands-a-review-subagent-and-hooks-404j</guid>
      <description>&lt;p&gt;Claude Code is powerful the moment you install it. But the thing that makes it feel like a real teammate isn't the model — it's the configuration you build up around it: custom slash commands, specialist subagents, and hooks that run at the right moments.&lt;/p&gt;

&lt;p&gt;The problem is that a fresh install is a blank &lt;code&gt;.claude/&lt;/code&gt; folder. You get all that value only after you read the docs and write the config yourself.&lt;/p&gt;

&lt;p&gt;So here's the config I now copy into every repo. Everything below is a plain file you can paste, read, and change. I'll show you real commands, a subagent, and a hook — with the actual code and why each part is there — so you can rebuild this yourself in an afternoon.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unofficial, community-made. Not affiliated with or endorsed by Anthropic.&lt;/strong&gt; "Claude" and "Claude Code" are trademarks of Anthropic. This is independent config &lt;em&gt;for&lt;/em&gt; Claude Code, not a product &lt;em&gt;of&lt;/em&gt; Anthropic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where this config lives
&lt;/h2&gt;

&lt;p&gt;Two locations, and you can use both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.claude/&lt;/code&gt; in your home directory — applies to &lt;strong&gt;every&lt;/strong&gt; project you open.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/&lt;/code&gt; in a repo root — applies to &lt;strong&gt;that&lt;/strong&gt; project, and you can commit it to share with your team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a project file and a global file collide, the project one wins. I keep general-purpose commands and subagents in &lt;code&gt;~/.claude/&lt;/code&gt;, and project-specific rules in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A slash command is just a markdown file
&lt;/h2&gt;

&lt;p&gt;The file name is the command name: &lt;code&gt;.claude/commands/review.md&lt;/code&gt; becomes &lt;code&gt;/review&lt;/code&gt;. The body is the prompt Claude runs. Here's a full, useful one — a pre-commit code review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;changes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;bugs,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;risks&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;quality&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;you&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;commit"&lt;/span&gt;
&lt;span class="na"&gt;argument-hint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;optional path or focus&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash(git status:*), Bash(git diff:*), Bash(git log:*)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Changed files: !&lt;span class="sb"&gt;`git status --short`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Full diff: !&lt;span class="sb"&gt;`git diff HEAD`&lt;/span&gt;

&lt;span class="gu"&gt;## Task&lt;/span&gt;

Act as a careful senior reviewer of the diff above (focus on "$ARGUMENTS" if provided).

Report findings grouped by severity — &lt;span class="gs"&gt;**Blocking**&lt;/span&gt;, &lt;span class="gs"&gt;**Should-fix**&lt;/span&gt;, &lt;span class="gs"&gt;**Nit**&lt;/span&gt; — and for
each give the &lt;span class="sb"&gt;`file:line`&lt;/span&gt;, what is wrong, and a concrete fix. Look specifically for:
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Correctness**&lt;/span&gt;: off-by-one, null/undefined, wrong operators, missed edge cases.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Security**&lt;/span&gt;: injection, unsafe input handling, secrets committed, missing auth checks.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Concurrency &amp;amp; resources**&lt;/span&gt;: races, leaks, unclosed handles, missing timeouts.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Contracts**&lt;/span&gt;: API changes that break callers or aren't reflected in tests/docs.

If the diff is clean, say so plainly instead of inventing problems. End with a one-line
verdict: safe to commit, or not yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things are doing the work here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;!&lt;/code&gt; runs a shell command and injects its output into the prompt.&lt;/strong&gt; &lt;code&gt;!`git diff HEAD`&lt;/code&gt; means Claude reviews your &lt;em&gt;actual&lt;/em&gt; current diff, not a description of it. This is the single most useful trick in the whole system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allowed-tools&lt;/code&gt;&lt;/strong&gt; pre-approves exactly those git commands, so &lt;code&gt;/review&lt;/code&gt; doesn't stop to ask permission every time it reads the diff. (If Claude Code still prompts you all the time, I wrote a &lt;a href="https://dev.to/vincentchabran/why-claude-code-keeps-asking-for-permission-and-how-to-fix-your-setup-so-it-stops-3ho8"&gt;deep-dive on the permission model and how to fix it&lt;/a&gt; — including a trap that makes a command prompt on every run even when everything looks allowed.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;$ARGUMENTS&lt;/code&gt;&lt;/strong&gt; is whatever you type after the command. &lt;code&gt;/review the auth changes&lt;/code&gt; focuses the review; &lt;code&gt;/review&lt;/code&gt; alone reviews everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now &lt;code&gt;/review&lt;/code&gt; gives you a consistent, senior-style pass over your changes every time, instead of a fuzzy "can you check this?"&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Bake your repo's conventions into &lt;code&gt;/commit&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Same idea, pointed at commits. The trick here is feeding Claude your recent history so the new message &lt;em&gt;matches the style you already use&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create a well-formed git commit from the current changes&lt;/span&gt;
&lt;span class="na"&gt;argument-hint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;optional message or intent&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;allowed-tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash(git status:*), Bash(git diff:*), Bash(git add:*), Bash(git commit:*), Bash(git log:*)&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Current status: !&lt;span class="sb"&gt;`git status --short`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Staged diff: !&lt;span class="sb"&gt;`git diff --cached`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Recent commits (match this style): !&lt;span class="sb"&gt;`git log --oneline -10`&lt;/span&gt;

&lt;span class="gu"&gt;## Task&lt;/span&gt;

Create one focused git commit for the changes above.
&lt;span class="p"&gt;
1.&lt;/span&gt; If nothing is staged, stage the files that clearly belong together — never unrelated
   files, build artifacts, or anything that looks like a secret.
&lt;span class="p"&gt;2.&lt;/span&gt; Write a Conventional Commits message (&lt;span class="sb"&gt;`type(scope): summary`&lt;/span&gt;), imperative mood,
   ≤ 72 chars, matching the tone of the recent commits shown above.
&lt;span class="p"&gt;3.&lt;/span&gt; Commit with &lt;span class="sb"&gt;`git commit`&lt;/span&gt;. Do &lt;span class="gs"&gt;**not**&lt;/span&gt; push. No co-author trailers or tool ads.

If the staged changes cover several unrelated concerns, say so and propose splitting them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;!`git log --oneline -10`&lt;/code&gt; injects your last ten commits, Claude writes in &lt;em&gt;your&lt;/em&gt; house style — not a generic &lt;code&gt;chore: update files&lt;/code&gt;. And "do not push" keeps a human in the loop for the one step that leaves your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Subagents: specialists with their own context window
&lt;/h2&gt;

&lt;p&gt;A subagent is a markdown file in &lt;code&gt;.claude/agents/&lt;/code&gt; with a bit of frontmatter and a system prompt. Claude can hand a task to it automatically, or you can call it by name. The win: the subagent works in a &lt;strong&gt;separate context window&lt;/strong&gt;, so a long review or audit doesn't clutter your main conversation.&lt;/p&gt;

&lt;p&gt;Here's the &lt;code&gt;code-reviewer&lt;/code&gt; I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-reviewer&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Expert code reviewer. Use PROACTIVELY right after writing or changing code,&lt;/span&gt;
  &lt;span class="s"&gt;and before committing, to catch correctness, security and design issues.&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Read, Grep, Glob, Bash&lt;/span&gt;
&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

You are a senior software engineer doing a focused, high-signal code review.
Your job is to catch what matters and say it plainly.

&lt;span class="gu"&gt;## What you look for, in priority order&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Correctness**&lt;/span&gt; — logic errors, off-by-one, null/undefined, unhandled error paths.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Security**&lt;/span&gt; — untrusted input reaching queries/commands/filesystem, injection,
   missing auth, secrets committed.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Contracts**&lt;/span&gt; — API changes that break callers or aren't reflected in tests/docs.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Resource &amp;amp; concurrency**&lt;/span&gt; — leaks, races, missing timeouts.
&lt;span class="p"&gt;5.&lt;/span&gt; &lt;span class="gs"&gt;**Design &amp;amp; readability**&lt;/span&gt; — duplication, dead code, unclear names. Lower priority.

&lt;span class="gu"&gt;## Rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Do not invent problems to seem thorough. If the code is solid, say so and stop.
&lt;span class="p"&gt;-&lt;/span&gt; End with a one-line verdict: &lt;span class="gs"&gt;**safe to merge**&lt;/span&gt;, &lt;span class="gs"&gt;**safe after should-fixes**&lt;/span&gt;, or &lt;span class="gs"&gt;**not yet**&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;description&lt;/code&gt; is how Claude decides when to delegate.&lt;/strong&gt; Writing it as "use PROACTIVELY right after writing code" is what makes the subagent trigger on its own. Vague descriptions never fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tools&lt;/code&gt; scopes what it can touch.&lt;/strong&gt; A reviewer only needs to read and search (&lt;code&gt;Read, Grep, Glob, Bash&lt;/code&gt;) — it shouldn't be editing your files. Omit &lt;code&gt;tools&lt;/code&gt; entirely and it inherits everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After this is in place, Claude tends to call the reviewer itself after a change. You can always force it: &lt;em&gt;"have the code-reviewer look at the auth module."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Hooks: automate what you'd otherwise forget
&lt;/h2&gt;

&lt;p&gt;Hooks run &lt;strong&gt;your&lt;/strong&gt; shell commands at defined moments in a session. They live in &lt;code&gt;settings.json&lt;/code&gt;. The one I'd never work without formats every file the instant Claude edits it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write|MultiEdit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"f=$(jq -r '.tool_input.file_path // empty'); [ -z &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ] &amp;amp;&amp;amp; exit 0; case &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; in *.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.scss|*.html|*.md|*.yml|*.yaml) command -v prettier &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; prettier --write &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 ;; *.py) command -v black &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; black -q &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 ;; *.go) command -v gofmt &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; gofmt -w &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 ;; *.rs) command -v rustfmt &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; rustfmt &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$f&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 ;; esac; exit 0"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to read that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PostToolUse&lt;/code&gt; + &lt;code&gt;matcher: "Edit|Write|MultiEdit"&lt;/code&gt;&lt;/strong&gt; means "run after Claude edits or writes a file." The matcher is a regex on the tool name.&lt;/li&gt;
&lt;li&gt;Claude Code sends the hook a &lt;strong&gt;JSON payload on stdin&lt;/strong&gt;. Here &lt;code&gt;jq&lt;/code&gt; pulls out &lt;code&gt;.tool_input.file_path&lt;/code&gt; — the file that was just touched.&lt;/li&gt;
&lt;li&gt;It picks a formatter by extension and &lt;strong&gt;no-ops silently if that formatter isn't installed&lt;/strong&gt;, so it never breaks a session on a machine that doesn't have &lt;code&gt;black&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit codes are the contract:&lt;/strong&gt; &lt;code&gt;0&lt;/code&gt; = fine, continue. &lt;code&gt;2&lt;/code&gt; = block the action and hand your message back to Claude as the reason. (A &lt;code&gt;PreToolUse&lt;/code&gt; hook that exits &lt;code&gt;2&lt;/code&gt; on &lt;code&gt;.env&lt;/code&gt; paths is a nice guard against Claude ever editing your secrets.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;jq&lt;/code&gt; is the only dependency (&lt;code&gt;brew install jq&lt;/code&gt; / &lt;code&gt;sudo apt install jq&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it in two minutes
&lt;/h2&gt;

&lt;p&gt;Grab the files, drop them in, restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/commands ~/.claude/agents
&lt;span class="nb"&gt;cp &lt;/span&gt;commands/&lt;span class="k"&gt;*&lt;/span&gt;.md ~/.claude/commands/
&lt;span class="nb"&gt;cp &lt;/span&gt;agents/&lt;span class="k"&gt;*&lt;/span&gt;.md   ~/.claude/agents/
&lt;span class="c"&gt;# then merge the hooks block into ~/.claude/settings.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Claude Code, type &lt;code&gt;/&lt;/code&gt;, and the commands are there. Run &lt;code&gt;/agents&lt;/code&gt; to see the subagent, &lt;code&gt;/hooks&lt;/code&gt; to confirm the hook loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  One habit that makes all of this better
&lt;/h2&gt;

&lt;p&gt;Notice a theme in the prompts above: they're written to be &lt;strong&gt;honest&lt;/strong&gt;. The review command is told not to invent problems. A companion &lt;code&gt;/pr&lt;/code&gt; command writes "not yet tested" instead of pretending. That matters — the fastest way to lose trust in an AI teammate is catching it claim a test passed when it never ran. Bake that into your prompts and the output stays trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the files
&lt;/h2&gt;

&lt;p&gt;I put the four commands, the &lt;code&gt;code-reviewer&lt;/code&gt; subagent, the auto-format hook, and a &lt;code&gt;CLAUDE.md&lt;/code&gt; template in a free, MIT-licensed repo so you can copy them straight in:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/VincentChabran/claude-code-starter-kit" rel="noopener noreferrer"&gt;Claude Code Starter Kit on GitHub&lt;/a&gt;&lt;/strong&gt; (MIT — use it, change it, ship it)&lt;/p&gt;

&lt;p&gt;If you end up wanting the full set — 12 commands, 6 subagents, 5 hooks, a git-aware status line, and a full CLAUDE.md template — there's a paid &lt;a href="https://vincentdu2a.gumroad.com/l/bqndfi" rel="noopener noreferrer"&gt;Power Pack&lt;/a&gt; that bundles it with a step-by-step guide. But the repo above is enough to get real value today.&lt;/p&gt;

&lt;p&gt;Happy building.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>One Block to Force a Default AWS Region Across Every Terragrunt Module</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Mon, 06 Jul 2026 17:32:57 +0000</pubDate>
      <link>https://dev.to/vincentchabran/one-block-to-force-a-default-aws-region-across-every-terragrunt-module-51ol</link>
      <guid>https://dev.to/vincentchabran/one-block-to-force-a-default-aws-region-across-every-terragrunt-module-51ol</guid>
      <description>&lt;p&gt;A quiet failure mode in multi-module Terragrunt setups: someone copies an existing module to bootstrap a new one, forgets to check the region in the provider config, and a resource lands in &lt;code&gt;us-east-1&lt;/code&gt; instead of &lt;code&gt;eu-west-1&lt;/code&gt; — three weeks later, someone's debugging why a Lambda can't reach a VPC that "should" be right there.&lt;/p&gt;

&lt;p&gt;This isn't a Terraform bug. It's a consequence of region being just another string that every module has to get right, independently, forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default pattern (and why it's fragile)
&lt;/h2&gt;

&lt;p&gt;Most Terragrunt setups declare region per-project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# projects/some-service/project.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;aws_region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the provider generation block in &lt;code&gt;root.hcl&lt;/code&gt; reads it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# root.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;read_terragrunt_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"project.hcl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="s2"&gt;"provider"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"provider.tf"&lt;/span&gt;
  &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite"&lt;/span&gt;
  &lt;span class="nx"&gt;contents&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
provider "aws" {
  region = "${local.project.locals.aws_region}"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing wrong with this in isolation. The problem shows up at scale: with 20+ modules, &lt;code&gt;aws_region&lt;/code&gt; is set 20+ times, by hand, by whoever created each module — usually by copy-pasting an existing &lt;code&gt;project.hcl&lt;/code&gt; and hoping they remembered to check every field. New modules silently inherit whatever the copy-paste source happened to have, correct or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: a default that wins unless explicitly overridden
&lt;/h2&gt;

&lt;p&gt;Flip the default. &lt;code&gt;root.hcl&lt;/code&gt; computes the region with a fallback, so a project only needs to declare &lt;code&gt;aws_region&lt;/code&gt; when it's an actual, deliberate exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# root.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;read_terragrunt_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"project.hcl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="c1"&gt;# Falls back to the org-wide default if the project doesn't override it.&lt;/span&gt;
  &lt;span class="nx"&gt;aws_region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;try&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="s2"&gt;"provider"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"provider.tf"&lt;/span&gt;
  &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite"&lt;/span&gt;
  &lt;span class="nx"&gt;contents&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
provider "aws" {
  region = "${local.aws_region}"
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a brand-new &lt;code&gt;project.hcl&lt;/code&gt; can be &lt;em&gt;empty&lt;/em&gt; on the region question and still deploy to the right place by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# projects/brand-new-service/project.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;legacy_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"brand-new-service"&lt;/span&gt;
  &lt;span class="c1"&gt;# no aws_region — inherits eu-west-1 from root.hcl automatically&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An exception is now a &lt;em&gt;visible, deliberate&lt;/em&gt; line, not a silent inheritance from whatever was copy-pasted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# projects/legacy-email-sender/project.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;legacy_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"legacy-email-sender"&lt;/span&gt;
  &lt;span class="nx"&gt;aws_region&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-north-1"&lt;/span&gt; &lt;span class="c1"&gt;# locked to this region — legacy third-party integration, see runbook §3&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is where the burden of proof sits. Before: every module has to actively get the region right. After: every module gets it right by doing nothing, and getting it "wrong" (i.e., different from the default) requires writing a line down and, ideally, a comment saying why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this generalizes
&lt;/h2&gt;

&lt;p&gt;Region is the clearest example because a wrong region is easy to notice, but the same shape applies to any setting that should be consistent across a growing set of modules and currently isn't enforced by anything except habit: default tags, default VPC, default log retention. Anywhere you find yourself saying "just copy an existing module's config and change what's different," that's a signal the &lt;em&gt;common&lt;/em&gt; part belongs in the root config with a &lt;code&gt;try()&lt;/code&gt; fallback, and only the actual differences belong in each project's own file.&lt;/p&gt;

&lt;p&gt;It costs one extra local in &lt;code&gt;root.hcl&lt;/code&gt;. It saves the debugging session three weeks from now.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Renaming Terragrunt Projects Without Migrating State</title>
      <dc:creator>VincentChabran</dc:creator>
      <pubDate>Mon, 06 Jul 2026 17:31:33 +0000</pubDate>
      <link>https://dev.to/vincentchabran/renaming-terragrunt-projects-without-migrating-state-52jg</link>
      <guid>https://dev.to/vincentchabran/renaming-terragrunt-projects-without-migrating-state-52jg</guid>
      <description>&lt;p&gt;If you've run Terragrunt across more than a handful of modules for a while, you've hit this: you want to reorganize your repo — rename a folder, group projects differently, fix a naming mistake from six months ago — and Terragrunt's default behavior punishes you for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;By default, Terragrunt derives your remote state key from the file path of the module. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# root.hcl&lt;/span&gt;
&lt;span class="nx"&gt;remote_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend.tf"&lt;/span&gt;
    &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-terraform-state-bucket"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path_relative_to_include()}/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;path_relative_to_include()&lt;/code&gt; is convenient until the day you move &lt;code&gt;projects/old-name/&lt;/code&gt; to &lt;code&gt;projects/new-name/&lt;/code&gt;. The key changes, Terraform can no longer find the state for those resources, and you're one &lt;code&gt;terragrunt apply&lt;/code&gt; away from either a pile of "already exists" errors or, worse, Terraform trying to recreate live infrastructure because it thinks it's gone.&lt;/p&gt;

&lt;p&gt;The usual fixes are painful: &lt;code&gt;terraform state mv&lt;/code&gt; per resource, or a full &lt;code&gt;terraform import&lt;/code&gt; pass. Both work, both are slow, both are one typo away from downtime on something that's just supposed to be a folder rename.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: decouple the state key from the file path
&lt;/h2&gt;

&lt;p&gt;Give every project a &lt;code&gt;project.hcl&lt;/code&gt; with a &lt;code&gt;legacy_prefix&lt;/code&gt; — the state key it should keep, independent of wherever the folder happens to live today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# projects/new-name/project.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;legacy_prefix&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"old-name"&lt;/span&gt;   &lt;span class="c1"&gt;# the prefix this project's state has always used&lt;/span&gt;
  &lt;span class="nx"&gt;aws_region&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then have &lt;code&gt;root.hcl&lt;/code&gt; build the state key from that prefix instead of the live path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# root.hcl&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;read_terragrunt_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;find_in_parent_folders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"project.hcl"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;remote_state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt;
  &lt;span class="nx"&gt;generate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backend.tf"&lt;/span&gt;
    &lt;span class="nx"&gt;if_exists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"overwrite"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-terraform-state-bucket"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${local.project.locals.legacy_prefix}/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the folder path is free to change. Rename it, move it, restructure the whole repo — the state key is pinned to &lt;code&gt;legacy_prefix&lt;/code&gt;, not to &lt;code&gt;path_relative_to_include()&lt;/code&gt;. A new project just sets &lt;code&gt;legacy_prefix&lt;/code&gt; to whatever it wants (usually matching its own path, since it has no history to preserve); an existing project keeps its old prefix forever, regardless of where the folder lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof, not just the theory
&lt;/h2&gt;

&lt;p&gt;Don't take "it should work" on faith. Before you consider a reorganization safe, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terragrunt plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;on every module you touched, and the only acceptable output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: 0 to add, 0 to change, 0 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything else means the key computation didn't land on the same value as before, and you stop right there. This one-line proof requirement is what makes the whole pattern trustworthy enough to use on live infrastructure: you're not hoping the refactor is safe, you're verifying it before it touches anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is worth it
&lt;/h2&gt;

&lt;p&gt;This pattern earns its keep the moment you have more than a few Terragrunt modules and expect to reorganize them more than once — a new team convention, a naming scheme that didn't survive contact with reality, projects that outgrew their original folder structure. If you have three modules that will never move, plain &lt;code&gt;path_relative_to_include()&lt;/code&gt; is simpler and you don't need this.&lt;/p&gt;

&lt;p&gt;The core idea generalizes beyond Terragrunt: whenever a tool derives a stable identifier (a state key, a resource ID, a cache key) from something that isn't actually stable (a file path, a display name), that's worth decoupling explicitly — before the first painful migration forces you to do it under pressure.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>terraform</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
