<?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: Rulestack</title>
    <description>The latest articles on DEV Community by Rulestack (@rulestack).</description>
    <link>https://dev.to/rulestack</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%2F4025074%2F8c45f5e9-1af0-48b9-9e5d-8078f7eb4043.png</url>
      <title>DEV Community: Rulestack</title>
      <link>https://dev.to/rulestack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rulestack"/>
    <language>en</language>
    <item>
      <title>Which CLAUDE.md Files Claude Code Actually Loads (and in What Order)</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Fri, 31 Jul 2026 02:33:16 +0000</pubDate>
      <link>https://dev.to/rulestack/which-claudemd-files-claude-code-actually-loads-and-in-what-order-3be0</link>
      <guid>https://dev.to/rulestack/which-claudemd-files-claude-code-actually-loads-and-in-what-order-3be0</guid>
      <description>&lt;p&gt;Half of the "Claude Code is ignoring my rules" reports I see have the same root cause: the rule lives in a file Claude Code never loaded. The memory system reads a specific set of files, in a specific order, with a few loading rules that are easy to get backwards. Here is the full lookup order, verified against the official memory docs (code.claude.com/docs/en/memory) on 2026-07-31.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four memory scopes
&lt;/h2&gt;

&lt;p&gt;Claude Code merges memory from four locations, listed here from broadest to narrowest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Path (macOS)&lt;/th&gt;
&lt;th&gt;Shared with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Managed policy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/Library/Application Support/ClaudeCode/CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Everyone in the org (IT-deployed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Just you, across all projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your team, via git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project local&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./CLAUDE.local.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Just you, this project (auto-gitignored)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details people miss:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CLAUDE.local.md&lt;/code&gt; is loaded after the shared &lt;code&gt;CLAUDE.md&lt;/code&gt; at the same level.&lt;/strong&gt; That makes it the right place for personal overrides — your sandbox URLs, your preferred test filters — without touching the team file.&lt;/li&gt;
&lt;li&gt;The managed policy file exists on Linux and Windows too, at different paths. If a rule appears that nobody on the team wrote, check there first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What loads at launch vs. on demand
&lt;/h2&gt;

&lt;p&gt;This is the part that explains most "why didn't it load" confusion:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At launch, Claude Code walks &lt;em&gt;up&lt;/em&gt; from your working directory.&lt;/strong&gt; Start it in &lt;code&gt;repo/apps/web&lt;/code&gt; and it loads &lt;code&gt;repo/apps/web/CLAUDE.md&lt;/code&gt;, &lt;code&gt;repo/apps/CLAUDE.md&lt;/code&gt;, &lt;code&gt;repo/CLAUDE.md&lt;/code&gt;, and so on above — all in full, concatenated from the root down to your cwd. Instructions closer to your working directory appear later in the context, closest to your actual task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subdirectory CLAUDE.md files do not load at launch.&lt;/strong&gt; A file like &lt;code&gt;repo/apps/web/tests/CLAUDE.md&lt;/code&gt; (below your cwd) only loads when Claude actually reads a file inside that subtree. This is by design — you can give each package its own conventions without paying the token cost until the agent goes there. But it also means: if you test your memory setup by asking "what are my rules?" at the repo root, the subdirectory files will look invisible. They are lazy, not broken.&lt;/p&gt;

&lt;p&gt;One more launch-time surprise: directories you attach with &lt;code&gt;--add-dir&lt;/code&gt; do &lt;strong&gt;not&lt;/strong&gt; get their CLAUDE.md loaded. That only happens if you set &lt;code&gt;CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Imports: the &lt;code&gt;@path&lt;/code&gt; syntax
&lt;/h2&gt;

&lt;p&gt;Any memory file can pull in other files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;See @README.md for the project overview and @package.json for scripts.

&lt;span class="gh"&gt;# Personal preferences&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; @~/.claude/my-project-instructions.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules that matter in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relative paths resolve relative to the file containing the import&lt;/strong&gt;, not your cwd. A &lt;code&gt;@./shared/rules.md&lt;/code&gt; inside &lt;code&gt;apps/web/CLAUDE.md&lt;/code&gt; points into &lt;code&gt;apps/web/shared/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Imports load at launch&lt;/strong&gt;, together with the importing file. Splitting a huge CLAUDE.md into imported chunks reorganizes it; it does not reduce the context cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max import depth is 4 hops.&lt;/strong&gt; Deep chains silently stop resolving past that.&lt;/li&gt;
&lt;li&gt;Imports inside code spans and code blocks are ignored — &lt;code&gt;`@anthropic-ai/claude-code`&lt;/code&gt; in a snippet will not trigger a file read.&lt;/li&gt;
&lt;li&gt;Importing a file from outside your project directory triggers an approval dialog the first time, so a malicious repo cannot silently pull your &lt;code&gt;~/.claude&lt;/code&gt; files into context.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using AGENTS.md without duplicating it
&lt;/h2&gt;

&lt;p&gt;Claude Code does not read &lt;code&gt;AGENTS.md&lt;/code&gt; natively. The documented pattern is a one-line project &lt;code&gt;CLAUDE.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;@AGENTS.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps one source of truth for multi-tool teams (Codex, Cursor agents, etc.) while Claude Code loads it through its normal import path. A symlink works too, with the caveat that creating symlinks on Windows requires admin rights — the import line avoids that entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoped rules in &lt;code&gt;.claude/rules/&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of one monolithic CLAUDE.md, you can split rules into &lt;code&gt;.claude/rules/*.md&lt;/code&gt;. Discovery is recursive, and individual rule files can be path-scoped so they only apply when Claude works on matching files. Combined with the documented target of keeping memory files under ~200 lines, this is the sane way to scale a large repo's conventions: a small always-on core, plus scoped rules that activate where they are relevant.&lt;/p&gt;

&lt;p&gt;For monorepos there is also &lt;code&gt;claudeMdExcludes&lt;/code&gt; in settings — a glob list that suppresses specific CLAUDE.md files from loading. It works at any settings layer, and the arrays merge across layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small print that bites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTML comments (&lt;code&gt;&amp;lt;!-- ... --&amp;gt;&lt;/code&gt;) are stripped before injection.&lt;/strong&gt; Useful for maintainer notes that should not spend tokens — but remember they &lt;em&gt;are&lt;/em&gt; preserved inside code blocks, so a commented example still costs what it weighs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto memory has a budget.&lt;/strong&gt; The &lt;code&gt;/memory&lt;/code&gt; command toggles it (&lt;code&gt;autoMemoryEnabled&lt;/code&gt; in settings), and only the first 200 lines / 25KB of &lt;code&gt;MEMORY.md&lt;/code&gt; are loaded each session. If your agent "forgets" something it wrote down, check whether it fell below that line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A 60-second debug checklist
&lt;/h2&gt;

&lt;p&gt;When a rule is not applying:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the file in one of the four scopes, or below your cwd? Below-cwd files load lazily.&lt;/li&gt;
&lt;li&gt;Started Claude Code from a subdirectory? Files &lt;em&gt;above&lt;/em&gt; you load; siblings do not.&lt;/li&gt;
&lt;li&gt;Is the rule past an import chain deeper than 4 hops?&lt;/li&gt;
&lt;li&gt;Is the import accidentally inside a code fence?&lt;/li&gt;
&lt;li&gt;Monorepo: is the file matched by a &lt;code&gt;claudeMdExcludes&lt;/code&gt; glob?&lt;/li&gt;
&lt;li&gt;Wrapped in an HTML comment outside a code block? Then it was stripped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every one of these has produced a real "the AI ignores my instructions" bug report that was actually a loading rule working exactly as documented.&lt;/p&gt;




&lt;p&gt;Most of the value in a rules file is knowing which lines earn their token cost — that is the curation work behind the &lt;a href="https://rulestack.gumroad.com/l/pykkol?ref=devto" rel="noopener noreferrer"&gt;AI Coding Starter&lt;/a&gt;, a $1 set of ten cross-tool rules already shaped for CLAUDE.md, &lt;code&gt;.cursorrules&lt;/code&gt;, and AGENTS.md.&lt;/p&gt;

&lt;p&gt;I post one field-tested note a day on AI coding workflows on Bluesky: &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>devtools</category>
      <category>programming</category>
    </item>
    <item>
      <title>What actually belongs in CLAUDE.md — and what to move to skills, hooks, or docs</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Thu, 30 Jul 2026 00:51:10 +0000</pubDate>
      <link>https://dev.to/rulestack/what-actually-belongs-in-claudemd-and-what-to-move-to-skills-hooks-or-docs-34id</link>
      <guid>https://dev.to/rulestack/what-actually-belongs-in-claudemd-and-what-to-move-to-skills-hooks-or-docs-34id</guid>
      <description>&lt;p&gt;Your CLAUDE.md only grows. Every incident adds a rule, every preference becomes a paragraph, and six months in you have a file that reads like a wiki — and an agent that follows maybe half of it.&lt;/p&gt;

&lt;p&gt;The uncomfortable mechanics: CLAUDE.md is loaded into context automatically, which means &lt;strong&gt;every line is a tax on every single request&lt;/strong&gt;, whether it's relevant or not. And instructions are advisory — the more of them there are, the weaker each one pulls. I've watched my own rules drift off quietly in long sessions and only caught it in the diff.&lt;/p&gt;

&lt;p&gt;So the real question isn't "what should I write in CLAUDE.md?" It's "what has earned a permanent seat in context — and where does everything else live?"&lt;/p&gt;

&lt;p&gt;Claude Code gives you four places to put guidance, and they have very different cost models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Loaded&lt;/th&gt;
&lt;th&gt;Costs tokens&lt;/th&gt;
&lt;th&gt;Enforcement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CLAUDE.md&lt;/td&gt;
&lt;td&gt;every session, automatically&lt;/td&gt;
&lt;td&gt;always&lt;/td&gt;
&lt;td&gt;advisory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill&lt;/td&gt;
&lt;td&gt;body only when the task matches&lt;/td&gt;
&lt;td&gt;on demand&lt;/td&gt;
&lt;td&gt;advisory, but scoped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hook&lt;/td&gt;
&lt;td&gt;never enters context as prose&lt;/td&gt;
&lt;td&gt;~zero&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;deterministic&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docs/ file&lt;/td&gt;
&lt;td&gt;when the agent (or a rule) points at it&lt;/td&gt;
&lt;td&gt;on demand&lt;/td&gt;
&lt;td&gt;none — it's reference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the sorting test I use, one sentence per destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-sentence test
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep it in CLAUDE.md if it's true on every turn, cheap to state, and expensive to violate.&lt;/strong&gt; Build commands, naming conventions, "tests live in &lt;code&gt;test/&lt;/code&gt;, mirrored by path", the tone your commit messages use. If you'd want the rule active even in the middle of an unrelated refactor, it's an &lt;em&gt;invariant&lt;/em&gt; — that's what the always-loaded file is for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move it to a skill if it starts with "when doing X".&lt;/strong&gt; Deploy checklists, a review rubric, the release-notes format, your migration playbook. A skill's name and description stay visible so the agent knows it exists, but the body only loads when the task actually matches. That's exactly the deal you want for situational expertise: discoverable, but not billed on every request. (Format details: &lt;a href="https://dev.to/rulestack/skillmd-how-to-write-a-claude-code-skill-that-actually-triggers-format-template-27cp"&gt;how to write a SKILL.md that actually triggers&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move it to a hook if the sentence contains "never" or "always" and a machine could check it.&lt;/strong&gt; Formatting before commit, protected paths, "don't touch &lt;code&gt;.env&lt;/code&gt;", "block pushes to main". Don't ask a language model to &lt;em&gt;remember&lt;/em&gt; what a shell script can &lt;em&gt;enforce&lt;/em&gt; — a hook fires deterministically whether the context is fresh or fifty files deep. (&lt;a href="https://dev.to/rulestack/claude-code-hooks-explained-config-structure-matchers-and-a-copy-paste-pretooluse-guard-58jj"&gt;Hook config structure and matchers&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move it to a docs file if it's something the agent should &lt;em&gt;read&lt;/em&gt;, not &lt;em&gt;obey&lt;/em&gt;.&lt;/strong&gt; Architecture overviews, API quirks, the history of why the billing module is weird. Put it in &lt;code&gt;docs/&lt;/code&gt;, and leave one line in CLAUDE.md saying when to read it: "Touching billing? Read &lt;code&gt;docs/billing-history.md&lt;/code&gt; first." Reference material rents context only when it's relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the default answer is "not CLAUDE.md"
&lt;/h2&gt;

&lt;p&gt;Three costs stack up on the always-loaded path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You pay it every request.&lt;/strong&gt; A 4,000-token CLAUDE.md is 4,000 tokens on every prompt, every tool call round-trip, all day. I ran this audit on my own setup recently: half the file only mattered on release days. Moving that half out cut every request's overhead and nothing broke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attention dilutes.&lt;/strong&gt; Twenty rules each get a sliver of the model's compliance budget. Five rules get real weight. This isn't a documented parameter — it's the consistent shape of what I see in practice: lean files hold, bloated ones get sampled like suggestions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long sessions erode prose.&lt;/strong&gt; As context fills with diffs and tool output, early instructions fade — and after compaction, what survives is a summary, not your exact wording. (&lt;a href="https://dev.to/rulestack/what-survives-compaction-in-claude-code-and-how-to-keep-your-rules-alive-564p"&gt;What survives compaction, and how to keep rules alive&lt;/a&gt;.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hooks dodge all three. Skills dodge the first two. Docs dodge all three but enforce nothing. CLAUDE.md is the only slot that pays full price — spend it on the few rules that deserve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a lean CLAUDE.md looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# myproject — agent guide&lt;/span&gt;

&lt;span class="gu"&gt;## Commands&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Build: &lt;span class="sb"&gt;`pnpm build`&lt;/span&gt; / Test: &lt;span class="sb"&gt;`pnpm test`&lt;/span&gt; (single file: &lt;span class="sb"&gt;`pnpm test path/to.test.ts`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Never run &lt;span class="sb"&gt;`pnpm deploy`&lt;/span&gt; directly — use the deploy skill.

&lt;span class="gu"&gt;## Conventions (always)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; TypeScript strict; no &lt;span class="sb"&gt;`any`&lt;/span&gt;. Errors are returned, not thrown, in &lt;span class="sb"&gt;`src/core`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; Tests mirror source paths. New modules need a test file in the same PR.
&lt;span class="p"&gt;-&lt;/span&gt; Commit messages: imperative mood, reference the issue.

&lt;span class="gu"&gt;## Enforced by hooks (listed so you know they exist)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Format-on-edit, protected paths (&lt;span class="sb"&gt;`.env`&lt;/span&gt;, &lt;span class="sb"&gt;`migrations/`&lt;/span&gt;), test gate before commit.

&lt;span class="gu"&gt;## Where things live&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Architecture: &lt;span class="sb"&gt;`docs/architecture.md`&lt;/span&gt; (read before cross-module changes)
&lt;span class="p"&gt;-&lt;/span&gt; Billing quirks: &lt;span class="sb"&gt;`docs/billing-history.md`&lt;/span&gt; (read before touching &lt;span class="sb"&gt;`src/billing`&lt;/span&gt;)

&lt;span class="gu"&gt;## When unsure&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Prefer asking over guessing on anything in &lt;span class="sb"&gt;`migrations/`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole shape: commands, invariants, a pointer to what's machine-enforced, a map, and an escalation rule. One screen. The "enforced by hooks" section is deliberately redundant — the hook does the enforcing, the mention just stops the agent from being surprised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating a bloated file in ~20 minutes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure it.&lt;/strong&gt; Paste your CLAUDE.md into a token counter. Write the number down — this is your before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Label every block&lt;/strong&gt; with one of four words: &lt;em&gt;always&lt;/em&gt; (true every turn), &lt;em&gt;situational&lt;/em&gt; ("when doing X"), &lt;em&gt;never-event&lt;/em&gt; (machine-checkable prohibition), &lt;em&gt;reference&lt;/em&gt; (explains, doesn't instruct).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move everything that isn't &lt;em&gt;always&lt;/em&gt;.&lt;/strong&gt; Situational → &lt;code&gt;.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt; with a description that names the trigger. Never-events → hooks or CI checks. Reference → &lt;code&gt;docs/&lt;/code&gt;, with a one-line pointer left behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a canary.&lt;/strong&gt; One harmless, visible rule — "start responses touching &lt;code&gt;src/billing&lt;/code&gt; with the word BILLING" — so you can tell at a glance whether the file is being honored at all. (&lt;a href="https://dev.to/rulestack/claude-code-hooks-not-firing-how-to-verify-what-actually-ran-44f"&gt;More ways to verify what actually ran&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-measure and re-audit monthly.&lt;/strong&gt; The file will try to grow back. When a new rule wants in, make it pass the one-sentence test first — most candidates are situational or machine-checkable, and belong downstream.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One honest caveat: none of this makes prose rules reliable. Advisory is advisory. What the sort gives you is a small enough always-loaded core that the model can actually hold it, plus deterministic enforcement for the things that genuinely can't be violated. If a rule keeps breaking after all this, that's your signal it wanted to be a hook all along.&lt;/p&gt;

&lt;p&gt;If you're not sure how your rules files get read in the first place — CLAUDE.md, AGENTS.md, and Cursor's rules all load differently — start with &lt;a href="https://dev.to/rulestack/how-cursor-claude-code-and-codex-actually-load-your-project-rules-and-why-yours-get-ignored-1l1j"&gt;how each tool actually loads your project rules&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Rulestack — I build and maintain drop-in rule, skill, and hook packs for Claude Code, Cursor, and Codex, at &lt;a href="https://rulestack.gumroad.com?ref=devto?ref=devto" rel="noopener noreferrer"&gt;rulestack.gumroad.com&lt;/a&gt;. I post working AI-coding tips on Bluesky at &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt; — follow along if this was useful.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Claude Code hooks not firing: how to verify what actually ran</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:28:44 +0000</pubDate>
      <link>https://dev.to/rulestack/claude-code-hooks-not-firing-how-to-verify-what-actually-ran-44f</link>
      <guid>https://dev.to/rulestack/claude-code-hooks-not-firing-how-to-verify-what-actually-ran-44f</guid>
      <description>&lt;p&gt;A hook that silently does nothing and a hook that isn't configured look identical from inside a session. Claude Code deliberately keeps successful hooks quiet — the transcript shows nothing when a hook exits 0 — so "I didn't see anything happen" tells you almost nothing about whether your guard actually ran.&lt;/p&gt;

&lt;p&gt;This is a checklist for closing that gap: how to confirm a hook is loaded, how to see what it did, and the specific failure modes that produce a hook which appears to work but doesn't.&lt;/p&gt;

&lt;p&gt;Everything below is from the current &lt;a href="https://code.claude.com/docs/en/hooks" rel="noopener noreferrer"&gt;hooks reference&lt;/a&gt; and &lt;a href="https://code.claude.com/docs/en/hooks-guide" rel="noopener noreferrer"&gt;hooks guide&lt;/a&gt;, read 2026-07-29. Where behavior changed by version I've said which version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: is it even loaded?
&lt;/h2&gt;

&lt;p&gt;Type &lt;code&gt;/hooks&lt;/code&gt; in Claude Code. It opens a read-only browser of your configured hooks: every event with a count, the matchers under it, and the full details of each handler — command, prompt, or URL.&lt;/p&gt;

&lt;p&gt;The detail worth going in for is &lt;strong&gt;which settings file each hook came from&lt;/strong&gt;. Hooks merge across user (&lt;code&gt;~/.claude/settings.json&lt;/code&gt;), project (&lt;code&gt;.claude/settings.json&lt;/code&gt;), local, and plugin (&lt;code&gt;hooks/hooks.json&lt;/code&gt;) sources. A hook you "definitely wrote" might be the project copy while the one actually running came from a plugin, or vice versa.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;/hooks&lt;/code&gt; shows nothing after you edited a settings file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File edits are normally picked up automatically. If nothing appears after a few seconds, the file watcher may have missed the change — restart the session to force a reload.&lt;/li&gt;
&lt;li&gt;Check the JSON is valid. Trailing commas and comments are not allowed, and a settings file that fails to parse takes your hooks down with it.&lt;/li&gt;
&lt;li&gt;Confirm the path. &lt;code&gt;.claude/settings.json&lt;/code&gt; for project hooks, &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for global.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: it's loaded but never runs
&lt;/h2&gt;

&lt;p&gt;The hook appears in &lt;code&gt;/hooks&lt;/code&gt; and still nothing happens. Three things account for most of these.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Matchers are case-sensitive.&lt;/strong&gt; &lt;code&gt;bash&lt;/code&gt; does not match the &lt;code&gt;Bash&lt;/code&gt; tool. This one is easy to stare past because the rest of the config looks right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The event isn't the one you think.&lt;/strong&gt; &lt;code&gt;PreToolUse&lt;/code&gt; fires before the tool executes, &lt;code&gt;PostToolUse&lt;/code&gt; after. If you wired a guard meant to block something to &lt;code&gt;PostToolUse&lt;/code&gt;, it runs — and can't undo anything, because the tool already ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;PermissionRequest&lt;/code&gt; hooks don't fire in non-interactive mode.&lt;/strong&gt; Run Claude Code headless with &lt;code&gt;-p&lt;/code&gt; and those hooks are skipped entirely. For automated permission decisions, use &lt;code&gt;PreToolUse&lt;/code&gt; instead. This is a common way a policy that works on your laptop is absent in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the exit code trap
&lt;/h2&gt;

&lt;p&gt;This is the failure mode I'd check first, because it produces a hook that runs, reports failure, and is ignored — which reads exactly like success.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;What Claude Code does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Success. stdout is parsed for JSON output fields. For most events stdout goes to the debug log, not the transcript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Blocking error.&lt;/strong&gt; stdout and any JSON in it are ignored; stderr is fed back to Claude as an error message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything else&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Non-blocking error.&lt;/strong&gt; The action proceeds. The transcript shows a &lt;code&gt;hook error&lt;/code&gt; notice with the first line of stderr; full stderr goes to the debug log&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trap is that &lt;strong&gt;&lt;code&gt;exit 1&lt;/code&gt; does not block.&lt;/strong&gt; It's the conventional Unix failure code, and for most hook events Claude Code treats it as a non-blocking error and proceeds with the action anyway. If your hook is meant to enforce a policy, it has to &lt;code&gt;exit 2&lt;/code&gt;.&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="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Wrong: the tool call proceeds anyway&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;bad_thing&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"not allowed"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Right: exit 2 blocks, and stderr becomes the reason Claude sees&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;bad_thing&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"not allowed: use the wrapper script instead"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one exception is &lt;code&gt;WorktreeCreate&lt;/code&gt;, where any non-zero exit code aborts worktree creation.&lt;/p&gt;

&lt;p&gt;Exit 2 does not mean the same thing everywhere, either. It blocks on events that represent an action that hasn't happened yet, and merely surfaces stderr on events describing something already done. &lt;code&gt;PostToolUse&lt;/code&gt; shows stderr to Claude but the tool already ran. &lt;code&gt;PermissionDenied&lt;/code&gt; ignores the exit code entirely because the denial already occurred. &lt;code&gt;SessionStart&lt;/code&gt;, &lt;code&gt;Setup&lt;/code&gt;, &lt;code&gt;SubagentStart&lt;/code&gt;, &lt;code&gt;Notification&lt;/code&gt;, &lt;code&gt;SessionEnd&lt;/code&gt; and friends show stderr to the user only — Claude never sees it.&lt;/p&gt;

&lt;p&gt;Two version boundaries here, both worth knowing if you are debugging against an older install:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As of &lt;strong&gt;v2.1.214&lt;/strong&gt;, a hook that exits 2 while printing JSON that fails schema validation still blocks — Claude Code uses stderr as the reason and records the validation failure in the debug log. Before that, this combination was treated as a non-blocking error and the action proceeded.&lt;/li&gt;
&lt;li&gt;As of &lt;strong&gt;v2.1.199&lt;/strong&gt;, &lt;code&gt;SessionStart&lt;/code&gt;, &lt;code&gt;Setup&lt;/code&gt; and &lt;code&gt;SubagentStart&lt;/code&gt; show exit-code-2 stderr in the transcript. Earlier versions wrote it to the debug log only, so a broken session hook was invisible unless you went looking.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: read what actually happened
&lt;/h2&gt;

&lt;p&gt;The transcript view (&lt;code&gt;Ctrl+O&lt;/code&gt;) gives you a one-line summary per hook that fired. Success is silent; blocking errors show stderr; non-blocking errors show a &lt;code&gt;hook error&lt;/code&gt; notice with the first line of stderr. That first line is often enough — you don't always need &lt;code&gt;--debug&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When it isn't enough, get the debug log, which has the full picture: which hooks matched, their exit codes, stdout, and stderr.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--debug-file&lt;/span&gt; /tmp/claude.log
&lt;span class="c"&gt;# then, in another terminal&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/claude.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Already mid-session without the flag? Run &lt;code&gt;/debug&lt;/code&gt; to enable logging and find the log path.&lt;/p&gt;

&lt;p&gt;A useful habit while developing: have the hook write a marker to stderr. stderr goes to the debug log without polluting stdout, which matters because stdout is where JSON output is parsed from.&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="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[my-hook] fired for &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_name'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: test the script outside Claude Code
&lt;/h2&gt;

&lt;p&gt;Hooks receive their JSON on stdin, so you can run one by hand. This separates "my script is broken" from "Claude Code isn't calling my script":&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;echo&lt;/span&gt; &lt;span class="s1"&gt;'{"tool_name":"Bash","tool_input":{"command":"ls"}}'&lt;/span&gt; | ./my-hook.sh
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;   &lt;span class="c"&gt;# is this the exit code you expected?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that works and the hook still doesn't fire correctly, the usual suspects are environment rather than logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;command not found&lt;/code&gt;&lt;/strong&gt; — hooks don't inherit your interactive shell's PATH assumptions. Use absolute paths or &lt;code&gt;${CLAUDE_PROJECT_DIR}&lt;/code&gt;. To sidestep shell quoting entirely, add &lt;code&gt;"args": []&lt;/code&gt; to switch to exec form, which spawns the script directly without a shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jq: command not found&lt;/code&gt;&lt;/strong&gt; — install &lt;code&gt;jq&lt;/code&gt;, or parse the JSON with Python or Node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing runs at all&lt;/strong&gt; — &lt;code&gt;chmod +x ./my-hook.sh&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one that looks impossible: "JSON validation failed"
&lt;/h2&gt;

&lt;p&gt;Your hook prints valid JSON. You've checked it. Claude Code says it can't parse it.&lt;/p&gt;

&lt;p&gt;The cause is usually not your script. A shell-form command hook (one without &lt;code&gt;args&lt;/code&gt;) is spawned via &lt;code&gt;sh -c&lt;/code&gt; on macOS and Linux, or Git Bash on Windows. That shell is non-interactive, but Git Bash — and any setup where &lt;code&gt;BASH_ENV&lt;/code&gt; points at &lt;code&gt;~/.bashrc&lt;/code&gt; — still sources your profile. If the profile prints anything unconditionally, it gets prepended to your JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shell ready on arm64
{"decision": "block", "reason": "Not allowed"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is in your shell profile, not the hook. Guard the output so it only runs in interactive shells:&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="c"&gt;# ~/.zshrc or ~/.bashrc&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$-&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;i&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Shell ready"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$-&lt;/code&gt; holds the shell's flags, and &lt;code&gt;i&lt;/code&gt; means interactive. Hooks run non-interactively, so the echo is skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more that produce confusing symptoms
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your Stop hook stopped working after eight tries.&lt;/strong&gt; Claude Code overrides a &lt;code&gt;Stop&lt;/code&gt; hook that blocks eight consecutive times without progress, and ends the turn with a warning. The hook is meant to check whether it already triggered a continuation — read &lt;code&gt;stop_hook_active&lt;/code&gt; from the JSON input and exit early when it's &lt;code&gt;true&lt;/code&gt;. If you genuinely need more iterations, raise the cap with &lt;code&gt;CLAUDE_CODE_STOP_HOOK_BLOCK_CAP&lt;/code&gt;.&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="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.stop_hook_active'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0   &lt;span class="c"&gt;# let Claude stop&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="c"&gt;# ... your logic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also worth knowing: &lt;code&gt;Stop&lt;/code&gt; fires whenever Claude finishes responding, not only at task completion, and it does not fire on user interrupts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your hook read the transcript and got stale data.&lt;/strong&gt; The &lt;code&gt;transcript_path&lt;/code&gt; in the hook input is written asynchronously and can lag the in-memory conversation, so it may not include the current turn's most recent messages. If you need the final assistant text of the current turn, use &lt;code&gt;last_assistant_message&lt;/code&gt; on &lt;code&gt;Stop&lt;/code&gt; or &lt;code&gt;SubagentStop&lt;/code&gt; instead of reading the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two hooks rewrite the same tool input and the result is random.&lt;/strong&gt; When multiple &lt;code&gt;PreToolUse&lt;/code&gt; hooks return &lt;code&gt;updatedInput&lt;/code&gt;, the last one to finish wins — and because hooks run in parallel, the order is non-deterministic. Don't have two hooks modify the same tool's input.&lt;/p&gt;

&lt;p&gt;Timeouts are a quieter version of the same problem. Command, HTTP and MCP-tool hooks get 10 minutes by default, but &lt;code&gt;UserPromptSubmit&lt;/code&gt; lowers that to 30 seconds and &lt;code&gt;MessageDisplay&lt;/code&gt; to 10. Prompt hooks get 30 seconds, agent hooks 60. A hook that's fine on &lt;code&gt;PostToolUse&lt;/code&gt; can time out on &lt;code&gt;UserPromptSubmit&lt;/code&gt; without you changing anything about the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a hook can and can't enforce
&lt;/h2&gt;

&lt;p&gt;Worth having straight before you debug the wrong layer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PreToolUse&lt;/code&gt; hooks fire &lt;strong&gt;before any permission-mode check&lt;/strong&gt;, in every permission mode including &lt;code&gt;dontAsk&lt;/code&gt;. A hook returning &lt;code&gt;permissionDecision: "deny"&lt;/code&gt; blocks the tool even under &lt;code&gt;bypassPermissions&lt;/code&gt; or &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt;. That's what makes hooks useful for policy a user shouldn't be able to switch off.&lt;/p&gt;

&lt;p&gt;The reverse does not hold. A hook returning &lt;code&gt;"allow"&lt;/code&gt; doesn't bypass deny rules from settings, and it can't suppress the prompt for connector tools your organization set to &lt;code&gt;ask&lt;/code&gt;, or MCP tools marked &lt;code&gt;requiresUserInteraction&lt;/code&gt;. &lt;strong&gt;Hooks tighten restrictions; they don't loosen them.&lt;/strong&gt; If you're trying to make something auto-approve and a hook isn't doing it, the hook isn't the layer with the authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/hooks&lt;/code&gt; — is it loaded, and from which settings file?&lt;/li&gt;
&lt;li&gt;Matcher case, correct event, and not &lt;code&gt;PermissionRequest&lt;/code&gt; under &lt;code&gt;-p&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exit 2&lt;/code&gt;, not &lt;code&gt;exit 1&lt;/code&gt;, if it's supposed to block.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl+O&lt;/code&gt; for the one-liner, &lt;code&gt;claude --debug-file&lt;/code&gt; for everything.&lt;/li&gt;
&lt;li&gt;Pipe sample JSON to the script directly to isolate script bugs from wiring bugs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generalizable lesson, and the reason exit codes are first on that list: a check that fails open is worse than no check, because its output is indistinguishable from a check that passed. &lt;code&gt;exit 1&lt;/code&gt; produces exactly that shape — the script ran, decided no, and the tool call went ahead anyway.&lt;/p&gt;

&lt;p&gt;If you're writing hooks alongside skills, the trigger mechanics are a separate problem with the same character — I wrote up &lt;a href="https://dev.to/rulestack/too-many-claude-code-skills-how-the-listing-budget-decides-which-descriptions-claude-sees-4a6m"&gt;how the listing budget decides which skill descriptions Claude actually sees&lt;/a&gt; and &lt;a href="https://dev.to/rulestack/claude-code-hooks-explained-config-structure-matchers-and-a-copy-paste-pretooluse-guard-58jj"&gt;the config structure and matchers for hooks themselves&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Rulestack — I build and maintain drop-in rule, skill, and hook packs for Claude Code, Cursor, and Codex, at &lt;a href="https://rulestack.gumroad.com?ref=devto" rel="noopener noreferrer"&gt;rulestack.gumroad.com&lt;/a&gt;. I post working AI-coding tips on Bluesky at &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt; — follow along if this was useful.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>debugging</category>
      <category>ai</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Too many Claude Code skills? How the listing budget decides which descriptions Claude sees</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:30:18 +0000</pubDate>
      <link>https://dev.to/rulestack/too-many-claude-code-skills-how-the-listing-budget-decides-which-descriptions-claude-sees-4a6m</link>
      <guid>https://dev.to/rulestack/too-many-claude-code-skills-how-the-listing-budget-decides-which-descriptions-claude-sees-4a6m</guid>
      <description>&lt;p&gt;You add a skill. It triggers. You add ten more, and the first one quietly stops firing — same file, same description, same request. Nothing errored, so there is nothing to fix.&lt;/p&gt;

&lt;p&gt;This is not a prompt problem. It is a budget problem, and Claude Code exposes every number involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Claude actually sees
&lt;/h2&gt;

&lt;p&gt;Claude Code loads a listing of skill names and descriptions into context so the model knows what is available. The listing always contains every skill name. Descriptions are the part under pressure.&lt;/p&gt;

&lt;p&gt;Two limits stack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Per entry.&lt;/strong&gt; The combined &lt;code&gt;description&lt;/code&gt; and &lt;code&gt;when_to_use&lt;/code&gt; text is truncated at 1,536 characters in the listing. &lt;code&gt;when_to_use&lt;/code&gt; is appended to &lt;code&gt;description&lt;/code&gt; and counts toward the same cap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Across all entries.&lt;/strong&gt; The whole listing has a character budget that scales at 1% of the model's context window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second one is what changes when you grow from 8 skills to 30. Your description did not get worse; it got crowded out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gets dropped first
&lt;/h2&gt;

&lt;p&gt;When the listing overflows, Claude Code drops descriptions starting with the skills you invoke least. The skills you use most keep their full text.&lt;/p&gt;

&lt;p&gt;Read that ordering carefully, because it is the opposite of what you want when debugging. The skill that stopped triggering is, by definition, one you have not been invoking — so it is first in line to lose its description, which makes it even less likely to trigger. It is a ratchet.&lt;/p&gt;

&lt;p&gt;A skill with no description in the listing is still there and still invokable by name. It just no longer advertises what it is for, so Claude cannot match it to a request on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring it instead of guessing
&lt;/h2&gt;

&lt;p&gt;Two commands, and they answer different questions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/doctor&lt;/code&gt; gives an estimate of the listing's context cost and its biggest contributors. Start here: it tells you which skills are eating the budget, which is what you need before deciding what to trim.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/context&lt;/code&gt; has a Skills row that reports the size of the listing &lt;strong&gt;after&lt;/strong&gt; the budget is applied, so it matches what the model actually receives. This matters if you have older habits: before v2.1.196, that row counted the full text of every description and could show a value several times larger than the configured budget. If you remember the Skills row being alarmingly big and stopped looking at it, look again.&lt;/p&gt;

&lt;p&gt;When the listing exceeds its budget, Claude Code also writes a warning to the debug log, visible with &lt;code&gt;--debug&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to fix it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Trim at the source, front-loading the use case.&lt;/strong&gt; Each entry is capped at 1,536 characters regardless of budget, and truncation takes the tail. That makes sentence order functional rather than stylistic: the concrete trigger case belongs in the first sentence, the caveats and background at the end where they are cheapest to lose. If you cap out at 1,536 characters on a single skill, that skill's description is doing a job the skill body should be doing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demote the skills you always call by name.&lt;/strong&gt; &lt;code&gt;skillOverrides&lt;/code&gt; controls skill visibility from settings rather than the skill's own frontmatter, which is what you want for skills checked into a shared repo you would rather not edit. Setting an entry to &lt;code&gt;"name-only"&lt;/code&gt; lists the skill without a description: it stays invokable as &lt;code&gt;/thing&lt;/code&gt;, it just stops spending description budget. &lt;code&gt;"off"&lt;/code&gt; removes it from the listing entirely.&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;"skillOverrides"&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;"release-checklist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name-only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"legacy-migration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&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;A skill absent from &lt;code&gt;skillOverrides&lt;/code&gt; is treated as &lt;code&gt;"on"&lt;/code&gt;. The &lt;code&gt;/skills&lt;/code&gt; menu writes this file for you — highlight a skill, press &lt;code&gt;Space&lt;/code&gt; to cycle states, &lt;code&gt;Enter&lt;/code&gt; to save to &lt;code&gt;.claude/settings.local.json&lt;/code&gt;. Plugin skills are not affected by &lt;code&gt;skillOverrides&lt;/code&gt;; manage those through &lt;code&gt;/plugin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raise the budget, once you know why.&lt;/strong&gt; &lt;code&gt;skillListingBudgetFraction&lt;/code&gt; takes a fraction of the context window (&lt;code&gt;0.02&lt;/code&gt; for 2%), or &lt;code&gt;SLASH_COMMAND_TOOL_CHAR_BUDGET&lt;/code&gt; sets a fixed character count. The per-entry cap is separately configurable with &lt;code&gt;skillListingMaxDescChars&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Raising the budget is the right move when you genuinely have many distinct skills. It is the wrong move when you have six skills that overlap, because you will pay context on all six every session to avoid choosing between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The overlap question
&lt;/h2&gt;

&lt;p&gt;Which brings up the failure mode a budget increase cannot fix. If two skills could plausibly answer the same request, the listing is now a coin flip, and the model resolves it from names and descriptions alone.&lt;/p&gt;

&lt;p&gt;The cheapest test: write down the request in the user's words, then read both descriptions and ask which one you would pick. If you hesitate, Claude will too. Either merge them, or add the negative case — "not for X, use &lt;code&gt;/y&lt;/code&gt; for that" — to whichever one loses more often. A sentence that says who a skill is &lt;em&gt;not&lt;/em&gt; for is usually worth more listing budget than another sentence about what it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more place descriptions matter
&lt;/h2&gt;

&lt;p&gt;Auto-compaction carries invoked skills forward within a token budget: when the conversation is summarized, Claude Code re-attaches the most recent invocation of each skill after the summary, keeping the first 5,000 tokens of each, with a combined budget of 25,000 tokens. It fills that budget starting from the most recently invoked skill.&lt;/p&gt;

&lt;p&gt;So in a long session where you invoked many skills, the oldest ones can be dropped entirely after compaction. If a skill's behavior seems to evaporate mid-session rather than never appearing at all, that is a different mechanism from the listing budget — and re-invoking it is the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Descriptions are capped at 1,536 characters per entry and share a listing budget of ~1% of the context window&lt;/li&gt;
&lt;li&gt;Overflow drops descriptions from your least-invoked skills first, which makes a quiet skill quieter&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/doctor&lt;/code&gt; finds the biggest contributors; the &lt;code&gt;/context&lt;/code&gt; Skills row shows the post-budget size&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"name-only"&lt;/code&gt; in &lt;code&gt;skillOverrides&lt;/code&gt; frees budget without deleting anything&lt;/li&gt;
&lt;li&gt;Raise the budget only after you have ruled out overlap between skills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All behavior above is from the Claude Code skills documentation as of July 2026; version-gated details (&lt;code&gt;/context&lt;/code&gt; accuracy in v2.1.196, &lt;code&gt;/doctor&lt;/code&gt; remaining typable in v2.1.205) are called out where they apply.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I maintain &lt;a href="https://rulestack.gumroad.com/?ref=devto" rel="noopener noreferrer"&gt;Rulestack&lt;/a&gt; — packs of rules, skills, and hooks for Claude Code, Cursor, and Codex, kept current with each tool's actual behavior.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I post one of these findings a day on Bluesky at &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt; — mostly the small mechanics that decide whether your config does anything.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>programming</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Your Claude Code skill's description is being cut: the 1,536-char cap and the shared listing budget</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Mon, 27 Jul 2026 08:35:40 +0000</pubDate>
      <link>https://dev.to/rulestack/your-claude-code-skills-description-is-being-cut-the-1536-char-cap-and-the-shared-listing-budget-4a77</link>
      <guid>https://dev.to/rulestack/your-claude-code-skills-description-is-being-cut-the-1536-char-cap-and-the-shared-listing-budget-4a77</guid>
      <description>&lt;p&gt;You wrote a skill. The description is clear. You ask for exactly the thing it does, and Claude reaches for something else.&lt;/p&gt;

&lt;p&gt;Before you rewrite the description a fourth time, check whether Claude ever saw the whole thing. Two separate limits trim skill metadata before the model reads it, and both are silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two limits are not the same limit
&lt;/h2&gt;

&lt;p&gt;Claude Code loads a &lt;strong&gt;listing&lt;/strong&gt; of your skills — names plus descriptions — into context at the start of a turn. That listing is how Claude knows what exists. Two independent caps apply to it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A per-entry cap.&lt;/strong&gt; Each skill's combined &lt;code&gt;description&lt;/code&gt; + &lt;code&gt;when_to_use&lt;/code&gt; text is capped at &lt;strong&gt;1,536 characters&lt;/strong&gt;. Past that, the tail is cut. This applies regardless of how much room the listing has left.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shared listing budget.&lt;/strong&gt; All entries together get a slice of the context window — 1% of it, with a documented fallback of 8,000 characters. When the listing overflows that slice, Claude Code starts dropping descriptions, beginning with the skills you invoke least. Your most-used skills keep their full text; the long tail turns into bare names.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The failure modes look different. Hit the per-entry cap and one skill loses the end of its trigger guidance — often the part listing the edge cases you care about. Hit the shared budget with 30 skills installed and an entire skill becomes a name with no description, which means Claude has nothing to match your request against.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it instead of guessing
&lt;/h2&gt;

&lt;p&gt;Two commands report on this directly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/doctor&lt;/code&gt; gives an estimate of the skill listing's context cost and names its biggest contributors. That is the fastest way to find out that one verbose skill is eating the budget everything else needs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/context&lt;/code&gt; has a Skills row that reports the listing size &lt;strong&gt;after&lt;/strong&gt; the budget is applied — so it matches what the model actually receives, not what you wrote. (Before v2.1.196 that row counted the full text of every description and could read several times larger than the configured budget, so if you remember this number being useless, it isn't anymore.)&lt;/p&gt;

&lt;p&gt;There is also a warning written to the debug log when the listing exceeds its budget. Run with &lt;code&gt;--debug&lt;/code&gt; if you want it in the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning the knobs
&lt;/h2&gt;

&lt;p&gt;If the diagnostics say you're budget-constrained, you have three moves, in rough order of how much I'd reach for them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trim at the source.&lt;/strong&gt; Put the key use case in the first sentence. Descriptions get cut from the end, so the ordering of your own text decides what survives. This is free and it makes the matching better anyway — a description that opens with "Diagnose a slow SQL query" matches more reliably than one that opens with three sentences of preamble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demote the skills you rarely invoke.&lt;/strong&gt; The &lt;code&gt;skillOverrides&lt;/code&gt; setting takes a per-skill value of &lt;code&gt;on&lt;/code&gt;, &lt;code&gt;name-only&lt;/code&gt;, &lt;code&gt;user-invocable-only&lt;/code&gt;, or &lt;code&gt;off&lt;/code&gt;. Setting a low-priority skill to &lt;code&gt;name-only&lt;/code&gt; lists it without a description, freeing that budget for skills that need to be matched automatically. A skill absent from &lt;code&gt;skillOverrides&lt;/code&gt; is treated as &lt;code&gt;on&lt;/code&gt;.&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;"skillOverrides"&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;"legacy-context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name-only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"off"&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;&lt;strong&gt;Raise the budget.&lt;/strong&gt; &lt;code&gt;skillListingBudgetFraction&lt;/code&gt; sets the fraction of the context window reserved for the listing — &lt;code&gt;0.02&lt;/code&gt; for 2%, default &lt;code&gt;0.01&lt;/code&gt;. The &lt;code&gt;SLASH_COMMAND_TOOL_CHAR_BUDGET&lt;/code&gt; environment variable pins a fixed character count instead. Raising it works, but you are spending context that the actual task would otherwise get, so do this after trimming, not before.&lt;/p&gt;

&lt;h2&gt;
  
  
  One naming discrepancy worth knowing
&lt;/h2&gt;

&lt;p&gt;The per-entry cap has a setting too, and here the two primary sources disagree on what it's called. The docs page names it &lt;code&gt;skillListingMaxDescChars&lt;/code&gt;. The published settings schema defines the same mechanism, with the same 1,536 default, as &lt;code&gt;maxSkillDescriptionChars&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'm flagging this because I got it wrong in the other direction recently: I checked one source, didn't find the name I expected, and concluded the setting didn't exist. It does. If you look for one name and come up empty, check the other before deciding the knob isn't there.&lt;/p&gt;

&lt;h2&gt;
  
  
  A debugging order that works
&lt;/h2&gt;

&lt;p&gt;When a skill won't trigger, this sequence finds the cause faster than rewriting prose:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;/doctor&lt;/code&gt;. If your listing is over budget, fix that first — nothing about your description matters if it was dropped.&lt;/li&gt;
&lt;li&gt;Check whether the skill's &lt;code&gt;description&lt;/code&gt; + &lt;code&gt;when_to_use&lt;/code&gt; is near 1,536 characters. If it is, the specifics at the end are gone.&lt;/li&gt;
&lt;li&gt;Read your description's &lt;strong&gt;first&lt;/strong&gt; sentence alone. If it's a noun phrase ("SQL helper") rather than a task ("Read SQL and propose indexes"), Claude has nothing concrete to match a user message against.&lt;/li&gt;
&lt;li&gt;Only then rewrite the body.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most "my skill doesn't trigger" reports I've traced end at step 3. But steps 1 and 2 are the ones you cannot find by staring at the file, which is exactly why they eat afternoons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this gets worse as you improve
&lt;/h2&gt;

&lt;p&gt;The uncomfortable part of the shared budget is that it punishes success. Every skill you add makes every other skill's description slightly more likely to be dropped. A setup that worked at 8 skills can quietly degrade at 25 without a single file changing. If you're building a library that grows, budget checks belong in your routine — not just in your debugging.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Verified against &lt;code&gt;https://code.claude.com/docs/en/skills&lt;/code&gt; and the published Claude Code settings schema on 2026-07-27. Behaviour and setting names move between versions — check your own before relying on a number here.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I write these while building &lt;a href="https://rulestack.gumroad.com/?ref=devto" rel="noopener noreferrer"&gt;Rulestack&lt;/a&gt;, packs of production-ready rules, skills, and hooks for Claude Code, Cursor, and Codex. I also post shorter findings like this one on Bluesky at &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt; — follow along if this is your kind of problem.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Claude Code hooks explained: config structure, matchers, and a copy-paste PreToolUse guard</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Fri, 24 Jul 2026 16:53:57 +0000</pubDate>
      <link>https://dev.to/rulestack/claude-code-hooks-explained-config-structure-matchers-and-a-copy-paste-pretooluse-guard-58jj</link>
      <guid>https://dev.to/rulestack/claude-code-hooks-explained-config-structure-matchers-and-a-copy-paste-pretooluse-guard-58jj</guid>
      <description>&lt;p&gt;If you have only ever used Claude Code's permission prompts, hooks are the next layer down: deterministic shell commands (or HTTP endpoints, MCP tools, prompts, or subagents) that fire at fixed points in a session and can &lt;strong&gt;block, allow, or reshape&lt;/strong&gt; what happens next. The feature is powerful, but the configuration has a few sharp edges that trip people up — especially the JSON your hook prints back, which changed and is easy to get wrong from memory.&lt;/p&gt;

&lt;p&gt;This post walks through the current config structure, the two ways a hook reports a decision, and a copy-paste &lt;code&gt;PreToolUse&lt;/code&gt; guard that blocks recursive &lt;code&gt;rm&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-level config structure
&lt;/h2&gt;

&lt;p&gt;Hooks live in a settings file (&lt;code&gt;.claude/settings.json&lt;/code&gt; for a project, &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for your user). The shape is three levels of nesting, and naming them makes the rest of the docs click:&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;"Bash"&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;"${CLAUDE_PROJECT_DIR}/.claude/hooks/block-rm.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"if"&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(rm *)"&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hook event&lt;/strong&gt; (&lt;code&gt;PreToolUse&lt;/code&gt;) — the lifecycle point. Events fall into three cadences: once per session (&lt;code&gt;SessionStart&lt;/code&gt;, &lt;code&gt;SessionEnd&lt;/code&gt;), once per turn (&lt;code&gt;UserPromptSubmit&lt;/code&gt;, &lt;code&gt;Stop&lt;/code&gt;), and on every tool call (&lt;code&gt;PreToolUse&lt;/code&gt;, &lt;code&gt;PostToolUse&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matcher group&lt;/strong&gt; (&lt;code&gt;"matcher": "Bash"&lt;/code&gt;) — a filter for &lt;em&gt;when&lt;/em&gt; the event fires. For tool events the matcher tests against &lt;code&gt;tool_name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook handler&lt;/strong&gt; (the inner &lt;code&gt;hooks&lt;/code&gt; array) — the thing that actually runs. There are five types: &lt;code&gt;command&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;mcp_tool&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;, and &lt;code&gt;agent&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;if&lt;/code&gt; field narrows further using permission-rule syntax — &lt;code&gt;Bash(rm *)&lt;/code&gt; here means "only spawn this handler when the Bash subcommand matches &lt;code&gt;rm *&lt;/code&gt;," so the script never even launches for &lt;code&gt;npm test&lt;/code&gt;. It holds exactly one rule; there is no &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; or &lt;code&gt;||&lt;/code&gt;, so multiple conditions mean multiple handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matchers are unanchored regexes (this bites people)
&lt;/h2&gt;

&lt;p&gt;A matcher that contains regex characters is tested with JavaScript's &lt;code&gt;RegExp.prototype.test&lt;/code&gt;, which succeeds on a match &lt;strong&gt;anywhere&lt;/strong&gt; in the value. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Edit&lt;/code&gt; also matches &lt;code&gt;NotebookEdit&lt;/code&gt;. If you mean only the &lt;code&gt;Edit&lt;/code&gt; tool, write &lt;code&gt;^Edit$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;MCP tools appear as &lt;code&gt;mcp__&amp;lt;server&amp;gt;__&amp;lt;tool&amp;gt;&lt;/code&gt;. A bare &lt;code&gt;mcp__memory&lt;/code&gt; is treated as an exact string and matches &lt;strong&gt;nothing&lt;/strong&gt; — you need the &lt;code&gt;.*&lt;/code&gt; suffix: &lt;code&gt;mcp__memory__.*&lt;/code&gt; for every tool from the &lt;code&gt;memory&lt;/code&gt; server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a handler to run on every occurrence of an event, omit the matcher or use &lt;code&gt;"*"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to report a decision — pick one
&lt;/h2&gt;

&lt;p&gt;This is where most confusion lives. A command hook communicates results through &lt;strong&gt;exit codes and stdout&lt;/strong&gt;, and you choose &lt;em&gt;one&lt;/em&gt; of two modes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exit codes alone.&lt;/strong&gt; Exit &lt;code&gt;0&lt;/code&gt; means success and no decision (staying silent does &lt;strong&gt;not&lt;/strong&gt; approve a call — it just lets the normal permission flow continue). Exit &lt;code&gt;2&lt;/code&gt; is a blocking error: Claude Code ignores stdout and feeds your &lt;strong&gt;stderr&lt;/strong&gt; back to Claude as the reason. For &lt;code&gt;PreToolUse&lt;/code&gt;, exit 2 blocks the tool call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exit 0 plus JSON on stdout.&lt;/strong&gt; For finer control, exit &lt;code&gt;0&lt;/code&gt; and print a JSON object. Claude Code only parses JSON on exit 0 — if you exit 2, any JSON is ignored. You must choose one approach per hook, not both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha: PreToolUse no longer uses &lt;code&gt;decision: "block"&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you learned hooks a while ago, you probably remember a top-level &lt;code&gt;{"decision": "block"}&lt;/code&gt;. &lt;strong&gt;That is deprecated for &lt;code&gt;PreToolUse&lt;/code&gt;.&lt;/strong&gt; This event now returns its decision inside a &lt;code&gt;hookSpecificOutput&lt;/code&gt; object with a required &lt;code&gt;hookEventName&lt;/code&gt;, and the field is &lt;code&gt;permissionDecision&lt;/code&gt; with four possible values: &lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;deny&lt;/code&gt;, &lt;code&gt;ask&lt;/code&gt;, or &lt;code&gt;defer&lt;/code&gt;. (The old &lt;code&gt;"approve"&lt;/code&gt;/&lt;code&gt;"block"&lt;/code&gt; map to &lt;code&gt;"allow"&lt;/code&gt;/&lt;code&gt;"deny"&lt;/code&gt;.) Other events like &lt;code&gt;PostToolUse&lt;/code&gt; and &lt;code&gt;Stop&lt;/code&gt; still use the top-level &lt;code&gt;decision&lt;/code&gt;/&lt;code&gt;reason&lt;/code&gt; fields — so the format genuinely differs per event, and copying a &lt;code&gt;PostToolUse&lt;/code&gt; snippet into &lt;code&gt;PreToolUse&lt;/code&gt; won't work.&lt;/p&gt;

&lt;p&gt;Here is the correct current shape for a &lt;code&gt;PreToolUse&lt;/code&gt; deny:&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;"hookSpecificOutput"&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;"hookEventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"permissionDecisionReason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recursive rm is blocked. Delete specific files instead."&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;h2&gt;
  
  
  Copy-paste: block recursive &lt;code&gt;rm&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;.claude/hooks/block-rm.sh&lt;/code&gt; and &lt;code&gt;chmod +x&lt;/code&gt; it. It reads the event JSON on stdin, pulls the Bash command with &lt;code&gt;jq&lt;/code&gt;, and denies anything with a recursive &lt;code&gt;rm&lt;/code&gt;:&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="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# PreToolUse hook: deny recursive rm, stay silent otherwise.&lt;/span&gt;
&lt;span class="c"&gt;# Requires jq on PATH.&lt;/span&gt;
&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.command // ""'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qiE&lt;/span&gt; &lt;span class="s1"&gt;'(^|[[:space:]])rm[[:space:]]+(-[a-z]*r|--recursive)'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="sh"&gt;'
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Recursive rm is blocked by a project hook. Delete specific files instead."
  }
}
&lt;/span&gt;&lt;span class="no"&gt;JSON
&lt;/span&gt;  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# No decision: exit 0 with no output lets the normal permission flow decide.&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the settings above, the &lt;code&gt;if: "Bash(rm *)"&lt;/code&gt; filter means the script only spawns for &lt;code&gt;rm&lt;/code&gt; commands (saving the process launch on unrelated calls), and the matcher keeps it scoped to Bash. When Claude tries &lt;code&gt;rm -rf /tmp/build&lt;/code&gt;, the hook prints the deny JSON, Claude Code blocks the call, and Claude sees the reason.&lt;/p&gt;

&lt;p&gt;A note on paths: &lt;code&gt;${CLAUDE_PROJECT_DIR}&lt;/code&gt; is substituted by Claude Code to your project root, so the hook resolves no matter what the working directory is at call time. If your project path contains spaces, switch the handler to exec form (set &lt;code&gt;args&lt;/code&gt;) so each element is passed as one argument with no shell quoting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;p&gt;Hooks are the deterministic guardrail layer. Two neighbors worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;if&lt;/code&gt; filter is best-effort.&lt;/strong&gt; For a &lt;em&gt;hard&lt;/em&gt; allow/deny that must never fail open, use the permission system rather than a hook — the docs are explicit that a malformed Bash command makes the &lt;code&gt;if&lt;/code&gt; filter fail open and run your hook anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills and subagents can define hooks in frontmatter&lt;/strong&gt;, scoped to that component's lifetime. If you are writing skills, the trigger mechanics matter as much as the hook wiring — I wrote up the &lt;a href="https://dev.to/rulestack/skillmd-how-to-write-a-claude-code-skill-that-actually-triggers-format-template-27cp"&gt;SKILL.md format and how to make a skill actually trigger&lt;/a&gt; and, separately, &lt;a href="https://dev.to/rulestack/custom-slash-commands-in-claude-code-how-they-work-now-that-commands-are-skills-425k"&gt;how custom slash commands work now that commands are skills&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Type &lt;code&gt;/hooks&lt;/code&gt; in Claude Code to open a read-only browser of everything you have configured, including which settings file each hook came from — a fast way to confirm your matcher is actually matching.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Rulestack — I build and maintain drop-in rule and hook packs for Claude Code, Cursor, and Codex. There's a &lt;a href="https://rulestack.gumroad.com?ref=devto" rel="noopener noreferrer"&gt;Claude Code Hooks Pack&lt;/a&gt; of production-ready guardrails if you'd rather not hand-roll each one. I post working AI-coding tips on Bluesky at &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt; — follow along if this was useful.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Custom slash commands in Claude Code: how they work now that commands are skills</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Thu, 23 Jul 2026 00:29:43 +0000</pubDate>
      <link>https://dev.to/rulestack/custom-slash-commands-in-claude-code-how-they-work-now-that-commands-are-skills-425k</link>
      <guid>https://dev.to/rulestack/custom-slash-commands-in-claude-code-how-they-work-now-that-commands-are-skills-425k</guid>
      <description>&lt;p&gt;You type &lt;code&gt;/fix-issue 123&lt;/code&gt; and Claude Code expands it into a full prompt with your team's standards attached. Custom slash commands are the cheapest automation in Claude Code — one markdown file, zero config — and they changed in a way that's easy to miss: &lt;strong&gt;custom commands have been merged into skills.&lt;/strong&gt; Your &lt;code&gt;.claude/commands/&lt;/code&gt; files keep working, but the merged model explains several things that look like bugs (arguments not expanding, tool grants vanishing mid-task) and adds features commands never had.&lt;/p&gt;

&lt;p&gt;Here is the current behavior, verified against the official docs as of late July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-file version still works
&lt;/h2&gt;

&lt;p&gt;Drop a markdown file into your project:&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="gh"&gt;&amp;lt;!-- .claude/commands/fix-issue.md --&amp;gt;
---
description: "Fix a GitHub issue by number"
---
&lt;/span&gt;Fix GitHub issue $ARGUMENTS following our coding standards.
Read the issue with &lt;span class="sb"&gt;`gh issue view`&lt;/span&gt;, locate the relevant code,
implement the fix, and add a regression test.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typing &lt;code&gt;/fix-issue 123&lt;/code&gt; sends that content with &lt;code&gt;$ARGUMENTS&lt;/code&gt; replaced by &lt;code&gt;123&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The merge means this file and a skill at &lt;code&gt;.claude/skills/fix-issue/SKILL.md&lt;/code&gt; both create &lt;code&gt;/fix-issue&lt;/code&gt; and behave the same way. What a skill adds on top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;directory&lt;/strong&gt; for supporting files (templates, scripts, reference docs Claude loads only when needed)&lt;/li&gt;
&lt;li&gt;frontmatter to control &lt;strong&gt;who&lt;/strong&gt; invokes it — you, Claude, or both&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;automatic invocation&lt;/strong&gt;: Claude can load it when your conversation matches its description&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a command and a skill share a name, the skill takes precedence. My practical rule: throwaway personal shortcuts can stay in &lt;code&gt;commands/&lt;/code&gt;; anything you will grow or share belongs in &lt;code&gt;skills/&lt;/code&gt;. (How to write a description that actually triggers is its own topic — I covered it in &lt;a href="https://dev.to/rulestack/skillmd-how-to-write-a-claude-code-skill-that-actually-triggers-format-template-27cp"&gt;the SKILL.md guide&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Arguments: &lt;code&gt;$ARGUMENTS&lt;/code&gt;, &lt;code&gt;$0&lt;/code&gt;, and the quoting rules
&lt;/h2&gt;

&lt;p&gt;Three expansion rules cover almost every confusion I see:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;$ARGUMENTS&lt;/code&gt; is the whole string as typed.&lt;/strong&gt; &lt;code&gt;/fix-issue 123 high-priority&lt;/code&gt; → &lt;code&gt;$ARGUMENTS&lt;/code&gt; becomes &lt;code&gt;123 high-priority&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Indexed access is zero-based and shell-quoted.&lt;/strong&gt; &lt;code&gt;$ARGUMENTS[0]&lt;/code&gt; (or the shorthand &lt;code&gt;$0&lt;/code&gt;) is the &lt;em&gt;first&lt;/em&gt; argument. Quoting groups words:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/migrate-component &lt;span class="s2"&gt;"search bar"&lt;/span&gt; React Vue
→ &lt;span class="nv"&gt;$0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; search bar, &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; React, &lt;span class="nv"&gt;$2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; Vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Missing values behave differently by placeholder type.&lt;/strong&gt; An indexed placeholder with no matching argument (&lt;code&gt;$2&lt;/code&gt; when you passed one arg) stays in the text &lt;em&gt;unchanged&lt;/em&gt;. A named placeholder declared in frontmatter expands to an &lt;em&gt;empty string&lt;/em&gt;. If you need a literal dollar-digit in prose (&lt;code&gt;$1.00&lt;/code&gt;), escape it: &lt;code&gt;\$1.00&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One more safety net: if you pass arguments but the file contains no &lt;code&gt;$ARGUMENTS&lt;/code&gt; at all, Claude Code appends &lt;code&gt;ARGUMENTS: &amp;lt;your input&amp;gt;&lt;/code&gt; to the end, so your input is never silently dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live data injection: &lt;code&gt;!`command`&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the feature that turns a canned prompt into a grounded one. Lines like:&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="gu"&gt;## Current diff&lt;/span&gt;
!&lt;span class="sb"&gt;`git diff HEAD`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;run the shell command &lt;em&gt;before&lt;/em&gt; Claude sees anything, and the output replaces the placeholder. Claude receives the actual diff, not an instruction to go fetch it — one fewer tool round-trip, and no chance of the model "summarizing" a diff it never read.&lt;/p&gt;

&lt;p&gt;Substitution is a single pass: a command's output cannot emit another &lt;code&gt;!`…`&lt;/code&gt; placeholder for a second expansion. Treat it as data, not macros.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;allowed-tools&lt;/code&gt; is a one-turn grant, not a session setting
&lt;/h2&gt;

&lt;p&gt;The most common surprise in the merged model. Frontmatter like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&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 add:*), Bash(git commit:*)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pre-approves those tools &lt;strong&gt;for the turn that invokes the skill&lt;/strong&gt;. The grant clears when you send your next message — even though the skill's &lt;em&gt;content&lt;/em&gt; stays in context. So "why is Claude asking permission again for the same command?" is usually this: the instructions persisted, the grant did not. Re-invoking the skill re-applies it.&lt;/p&gt;

&lt;p&gt;Two related notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;allowed-tools&lt;/code&gt; doesn't restrict anything. Unlisted tools remain available under your normal permission settings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;${CLAUDE_SKILL_DIR}&lt;/code&gt; expands in both the body and &lt;code&gt;allowed-tools&lt;/code&gt;, so a skill can ship a script and pre-approve exactly that script's invocation — no prompt, no wildcard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a grant that survives the whole session, that belongs in your permission rules instead — the allow/deny/ask matching logic is &lt;a href="https://dev.to/rulestack/claude-code-permission-rules-how-allow-deny-and-ask-actually-1cid"&gt;its own minefield&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding who can invoke it
&lt;/h2&gt;

&lt;p&gt;By default both you and Claude can run any skill. Two frontmatter switches change that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;disable-model-invocation: true&lt;/code&gt; — only you can trigger it. Use for anything with side effects where timing matters: &lt;code&gt;/deploy&lt;/code&gt;, &lt;code&gt;/commit&lt;/code&gt;, &lt;code&gt;/send-release-notes&lt;/code&gt;. You don't want the model deploying because the code "looks ready".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user-invocable: false&lt;/code&gt; — only Claude can load it. Use for background knowledge that isn't a meaningful action, like a &lt;code&gt;legacy-system-context&lt;/code&gt; skill.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Skill &lt;em&gt;descriptions&lt;/em&gt; are always in context so Claude knows what exists; the &lt;em&gt;body&lt;/em&gt; loads on invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The body persists — write it like standing orders
&lt;/h2&gt;

&lt;p&gt;Once invoked, the rendered content stays in the conversation for the rest of the session (re-invoking an identical skill adds a short "already loaded" note rather than a duplicate). Two consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every line is a recurring token cost. State what to do; skip the essay on why.&lt;/li&gt;
&lt;li&gt;Write instructions as standing rules ("always run X after editing Y"), not one-time steps — Claude does not re-read the file on later turns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After context compaction, recent skills are re-attached within a fixed token budget, so a long session can silently drop older ones — details in &lt;a href="https://dev.to/rulestack/what-survives-compaction-in-claude-code-and-how-to-keep-your-rules-alive-564p"&gt;what survives compaction&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a slash command is the wrong tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;fact&lt;/strong&gt; Claude should always know → one line in CLAUDE.md.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;procedure you keep pasting&lt;/strong&gt; → a skill. That's the sweet spot.&lt;/li&gt;
&lt;li&gt;Something that must happen &lt;strong&gt;every time, deterministically&lt;/strong&gt; → &lt;a href="https://dev.to/rulestack/your-claudemd-says-always-run-tests-hooks-are-how-you-actually-mean-it-1o9f"&gt;a hook&lt;/a&gt;. Skills rely on the model choosing to comply; hooks don't ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reference material too big for context&lt;/strong&gt; → skill with supporting files, referenced from SKILL.md and loaded on demand.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick gotcha checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$0&lt;/code&gt; is the &lt;strong&gt;first&lt;/strong&gt; argument (zero-based), quotes group words&lt;/li&gt;
&lt;li&gt;unmatched &lt;code&gt;$N&lt;/code&gt; stays literal; unmatched named args become empty&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;!`command`&lt;/code&gt; runs before Claude reads anything; single pass&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allowed-tools&lt;/code&gt; clears on your next message&lt;/li&gt;
&lt;li&gt;a project skill named like a bundled one (&lt;code&gt;code-review&lt;/code&gt;) &lt;strong&gt;replaces&lt;/strong&gt; it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/commands/&lt;/code&gt; still works; skills are the recommended path&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I maintain Rulestack — versioned rules &amp;amp; skills packs for Claude Code, Cursor, and Codex, kept in sync with changes like this one: &lt;a href="https://rulestack.gumroad.com?ref=devto" rel="noopener noreferrer"&gt;https://rulestack.gumroad.com?ref=devto&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily notes on AI coding workflows live on Bluesky: &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;https://bsky.app/profile/ai-shop.bsky.social&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>SKILL.md: how to write a Claude Code skill that actually triggers (format + template)</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Wed, 22 Jul 2026 05:41:02 +0000</pubDate>
      <link>https://dev.to/rulestack/skillmd-how-to-write-a-claude-code-skill-that-actually-triggers-format-template-27cp</link>
      <guid>https://dev.to/rulestack/skillmd-how-to-write-a-claude-code-skill-that-actually-triggers-format-template-27cp</guid>
      <description>&lt;p&gt;Claude Code skills answer a specific pain: you keep pasting the same checklist into chat, or a section of your CLAUDE.md has quietly grown from &lt;em&gt;facts about the project&lt;/em&gt; into &lt;em&gt;a multi-step procedure&lt;/em&gt;. The official guidance is exactly that — when instructions become a procedure you re-explain, move them into a skill. Unlike CLAUDE.md content, a skill's body loads only when it's used, so a long runbook costs you almost nothing until the moment you need it.&lt;/p&gt;

&lt;p&gt;This post covers the SKILL.md format end to end: the frontmatter fields, how triggering actually works, arguments, supporting files, and a template you can copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model: two-stage loading
&lt;/h2&gt;

&lt;p&gt;Everything about writing a good skill follows from one design fact:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The description is always visible.&lt;/strong&gt; Claude sees your skill's name and description in every session, and uses them to decide when the skill is relevant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The body loads only on use.&lt;/strong&gt; The instructions below the frontmatter enter context only when the skill fires (or when you invoke it with &lt;code&gt;/skill-name&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the description is not documentation — it's the &lt;strong&gt;trigger&lt;/strong&gt;. The body is not a summary — it's the &lt;strong&gt;procedure&lt;/strong&gt;. Most broken skills I've seen get these backwards: a vague description ("Helps with releases") that never matches anything, and a body that's a one-liner.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal working skill
&lt;/h2&gt;

&lt;p&gt;A skill is a directory with a &lt;code&gt;SKILL.md&lt;/code&gt; file. In a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;.claude/skills/release-notes/SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Personal skills live under &lt;code&gt;~/.claude/skills/&lt;/code&gt; and follow you across projects.)&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;release-notes&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;Draft&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;release&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;notes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;merged&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;PRs.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&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;user"&lt;/span&gt;
  &lt;span class="s"&gt;asks for release notes, a changelog entry, or "what shipped".&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Collect merged PRs since the last git tag, then write release notes.
&lt;span class="p"&gt;
1.&lt;/span&gt; Run &lt;span class="sb"&gt;`git describe --tags --abbrev=0`&lt;/span&gt; to find the last tag.
&lt;span class="p"&gt;2.&lt;/span&gt; List merges since then: &lt;span class="sb"&gt;`git log &amp;lt;tag&amp;gt;..HEAD --merges --oneline`&lt;/span&gt;.
&lt;span class="p"&gt;3.&lt;/span&gt; Group changes into Added / Fixed / Changed.
&lt;span class="p"&gt;4.&lt;/span&gt; Write entries as user-facing outcomes, not commit messages.
&lt;span class="p"&gt;5.&lt;/span&gt; Show the draft before writing to CHANGELOG.md.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a complete, working skill. Type &lt;code&gt;/release-notes&lt;/code&gt; to run it directly, or just ask "can you draft release notes?" and let the description do its job.&lt;/p&gt;

&lt;p&gt;If you've used custom commands before: &lt;strong&gt;commands and skills are now the same thing.&lt;/strong&gt; A file at &lt;code&gt;.claude/commands/deploy.md&lt;/code&gt; and a skill at &lt;code&gt;.claude/skills/deploy/SKILL.md&lt;/code&gt; both create &lt;code&gt;/deploy&lt;/code&gt; and behave the same way. Skills just add optional extras — a directory for supporting files and frontmatter to control invocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontmatter: four fields you'll actually use
&lt;/h2&gt;

&lt;p&gt;All fields are optional. Only &lt;code&gt;description&lt;/code&gt; is recommended — without it, Claude has nothing to match against, and the skill is effectively manual-only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&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;my-skill&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;What this skill does&lt;/span&gt;
&lt;span class="na"&gt;disable-model-invocation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;Read Grep&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;name&lt;/code&gt;&lt;/strong&gt; — the command name (&lt;code&gt;/my-skill&lt;/code&gt;). Defaults to the directory name, so most skills omit it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;description&lt;/code&gt;&lt;/strong&gt; — the trigger. Write it for the &lt;em&gt;matcher&lt;/em&gt;, not for humans browsing. Include the literal phrases people say: "Use when the user asks to X, Y, or mentions Z."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;disable-model-invocation: true&lt;/code&gt;&lt;/strong&gt; — Claude can never fire this on its own; only you can, via &lt;code&gt;/name&lt;/code&gt;. Use it for anything with side effects you want a human to initiate: deploys, commits, publishing, sending messages. This is the difference between a &lt;em&gt;reference&lt;/em&gt; skill (let Claude pull it in when relevant) and a &lt;em&gt;task&lt;/em&gt; skill (you pull the trigger).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allowed-tools&lt;/code&gt;&lt;/strong&gt; — pre-approve the tools the skill needs (e.g. &lt;code&gt;Read Grep&lt;/code&gt;), so a read-only helper doesn't stop to ask permission mid-run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last pairing is worth internalizing: &lt;strong&gt;knowledge skills → model-invoked; action skills → slash-only.&lt;/strong&gt; The frontmatter is where you encode that decision, and it's the single biggest safety lever in the format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arguments
&lt;/h2&gt;

&lt;p&gt;Skills take arguments when invoked as commands, with real substitution syntax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$ARGUMENTS&lt;/code&gt; — everything you passed. (If it doesn't appear in the body, arguments are appended as &lt;code&gt;ARGUMENTS: &amp;lt;value&amp;gt;&lt;/code&gt; instead.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$0&lt;/code&gt;, &lt;code&gt;$1&lt;/code&gt; — positional access, shorthand for &lt;code&gt;$ARGUMENTS[0]&lt;/code&gt;, &lt;code&gt;$ARGUMENTS[1]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named arguments&lt;/strong&gt; — declare &lt;code&gt;arguments: [issue, branch]&lt;/code&gt; in frontmatter and &lt;code&gt;$issue&lt;/code&gt; / &lt;code&gt;$branch&lt;/code&gt; expand by position. Self-documenting, so I'd default to this for anything with two or more parameters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;/fix-issue 1423 hotfix/login&lt;/code&gt; with &lt;code&gt;arguments: [issue, branch]&lt;/code&gt; gives you a body that reads like prose: "Fix issue &lt;code&gt;$issue&lt;/code&gt; on branch &lt;code&gt;$branch&lt;/code&gt;."&lt;/p&gt;

&lt;h2&gt;
  
  
  Supporting files: the 500-line rule
&lt;/h2&gt;

&lt;p&gt;Official guidance: &lt;strong&gt;keep SKILL.md under 500 lines&lt;/strong&gt; and move detailed reference material into separate files in the skill directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-skill/
├── SKILL.md        (required — overview and navigation)
├── reference.md    (detailed API docs — loaded when needed)
├── examples.md     (usage examples — loaded when needed)
└── scripts/
    └── helper.py   (executed, not loaded)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the &lt;em&gt;navigation&lt;/em&gt;: link each file from SKILL.md with a note about what it contains and when to read it —&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="gu"&gt;## Additional resources&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; For complete API details, see &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;reference.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;reference.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; For usage examples, see &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;examples.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;examples.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same two-stage trick again, one level down. SKILL.md stays a lean table of contents; the 2,000-line API reference costs nothing until Claude actually follows the link. Scripts are even cheaper — they get &lt;em&gt;executed&lt;/em&gt;, not read into context.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a skill won't trigger (or triggers too much)
&lt;/h2&gt;

&lt;p&gt;Almost every triggering problem is a description problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never fires:&lt;/strong&gt; the description describes the &lt;em&gt;domain&lt;/em&gt; ("Release management helpers") instead of the &lt;em&gt;moment&lt;/em&gt; ("Use when the user asks for release notes, a changelog, or what shipped"). Rewrite it around the phrases users actually say. Front-load them — long descriptions get truncated in listings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fires too often:&lt;/strong&gt; the description is too broad ("Use for anything git-related"). Narrow the trigger, or if it's a task skill that should never auto-fire anyway, set &lt;code&gt;disable-model-invocation: true&lt;/code&gt; and stop fighting the matcher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't tell if it loaded:&lt;/strong&gt; invoke it explicitly with &lt;code&gt;/skill-name&lt;/code&gt; once. If it works manually but never automatically, it's the description. It's almost always the description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more option worth knowing: skills can run in their own subagent context (&lt;code&gt;context: fork&lt;/code&gt;), which keeps a noisy multi-step procedure from flooding your main session. Useful for research-style skills that read a lot but should report back a summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lean starter (added by request)
&lt;/h2&gt;

&lt;p&gt;A reader asked for the leanest possible starting point — the version you paste in before you know what the skill will become:&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;my-skill&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;Use when I say "do X", "run X", or ask to X a file&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# My skill&lt;/span&gt;

Steps:
&lt;span class="p"&gt;1.&lt;/span&gt; ...
&lt;span class="p"&gt;2.&lt;/span&gt; ...

For edge cases, read &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;notes.md&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;notes.md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is genuinely enough to trigger. Add the rest only when it earns its place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;disable-model-invocation: true&lt;/code&gt; — once you notice it firing when you didn't ask&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;arguments&lt;/code&gt; — once you find yourself pasting the same values in by hand&lt;/li&gt;
&lt;li&gt;supporting files — once SKILL.md drifts past a hundred lines of edge cases&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context: fork&lt;/code&gt; — once the skill's output starts flooding your main session&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Portability note
&lt;/h2&gt;

&lt;p&gt;SKILL.md follows the Agent Skills open standard, and the same format is now read by several coding agents (Claude Code extends it with invocation control, subagent execution, and dynamic context injection). Writing your team runbooks in this format is a reasonably safe bet even if your tool mix changes later.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I maintain &lt;a href="https://rulestack.gumroad.com?ref=devto" rel="noopener noreferrer"&gt;Rulestack&lt;/a&gt;, a set of focused rule and skill packs for Claude Code, Cursor, and Codex — including a full Claude Code skill-authoring pack.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily AI-coding-agent tips on Bluesky: follow &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What survives compaction in Claude Code — and how to keep your rules alive</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Tue, 21 Jul 2026 14:08:02 +0000</pubDate>
      <link>https://dev.to/rulestack/what-survives-compaction-in-claude-code-and-how-to-keep-your-rules-alive-564p</link>
      <guid>https://dev.to/rulestack/what-survives-compaction-in-claude-code-and-how-to-keep-your-rules-alive-564p</guid>
      <description>&lt;p&gt;Long Claude Code sessions eventually hit the context window ceiling. When they do, Claude Code runs &lt;em&gt;compaction&lt;/em&gt;: it summarizes the conversation so far and starts a fresh window seeded with that summary. Work continues — but not everything you loaded at the start makes the trip.&lt;/p&gt;

&lt;p&gt;If you've ever watched an agent suddenly forget a rule it had been following for an hour, this is usually why. Here's what actually survives compaction, what quietly dies, and a workaround that keeps the important parts alive.&lt;/p&gt;

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

&lt;p&gt;Compaction is not truncation. Claude Code doesn't just drop old messages; it writes a summary of the session — decisions made, files touched, current task state — and rebuilds the context from that summary plus a small set of always-loaded material.&lt;/p&gt;

&lt;p&gt;That distinction matters because a summary is &lt;em&gt;lossy by design&lt;/em&gt;. It preserves the shape of the work ("we are migrating module X, tests are failing on Y") but not the exact text of everything that was ever in the window. Anything whose value depends on its exact wording — rules, checklists, contracts — is at risk unless it lives somewhere that gets reloaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What survives
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The root &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/strong&gt; It's re-read into the fresh window, so project-level instructions come back verbatim. This is the single most reliable place for a rule to live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unscoped rules&lt;/strong&gt; — the always-on portion of your rules setup — are retained the same way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The compaction summary itself&lt;/strong&gt;, which carries forward the task narrative and recent decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What dies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scoped rules&lt;/strong&gt; that were pulled in because you were working on matching files. After compaction, they're gone until something re-triggers them — and if the agent's next action doesn't match the scope, it acts without the rule it was just following.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mid-session instructions&lt;/strong&gt; you typed two hours ago. They exist only as whatever the summary kept of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Injected context&lt;/strong&gt; from earlier hook runs or tool output. The raw text is gone; only the summary's paraphrase remains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The scoped-rules case is the sneaky one. The whole point of scoping is to keep the always-loaded set small — but it also means your most specific, most carefully written rules are the ones with the shortest lifespan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workaround: SessionStart with &lt;code&gt;matcher: compact&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Claude Code's &lt;code&gt;SessionStart&lt;/code&gt; hook fires not just on a brand-new session but also right after compaction, when the matcher is set to &lt;code&gt;compact&lt;/code&gt;:&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;"SessionStart"&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;"compact"&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;"cat .claude/reinject.md"&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;Whatever that command prints to stdout is injected into the fresh window. The window turning over stops being an event you can't see and becomes a hook you can handle: re-state the invariants, re-load the working notes, re-assert the rule that must not be dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the re-injected payload small
&lt;/h2&gt;

&lt;p&gt;The tempting move is to re-inject everything — paste the full rules file back in after every compaction. Resist it. Compaction just reclaimed tokens; refilling the window with the same bulk material spends them again immediately, and you're back at the ceiling sooner.&lt;/p&gt;

&lt;p&gt;What works better is re-injecting an &lt;em&gt;index&lt;/em&gt;, not the state itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one line per invariant that must hold ("tests must pass before any commit"),&lt;/li&gt;
&lt;li&gt;pointers to where the detail lives ("full DB migration rules: &lt;code&gt;.claude/rules/db.md&lt;/code&gt;"),&lt;/li&gt;
&lt;li&gt;the current task state in two or three lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent can follow a pointer when it needs the detail. A 15-line reinject file costs almost nothing per compaction; a 400-line one costs you the headroom compaction just bought.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sort your rules by lifespan.&lt;/strong&gt; Anything that must hold for the whole session belongs in the root &lt;code&gt;CLAUDE.md&lt;/code&gt;, not in a scoped rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat scoped rules as ephemeral.&lt;/strong&gt; They're for file-specific detail the agent can afford to re-discover, not for invariants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;SessionStart&lt;/code&gt; hook with &lt;code&gt;matcher: compact&lt;/code&gt;&lt;/strong&gt; that prints your reinject index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the index under ~20 lines.&lt;/strong&gt; Pointers over payloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test it deliberately&lt;/strong&gt;: run &lt;code&gt;/compact&lt;/code&gt; manually mid-session and check whether the agent still honors the rule you care about. If it doesn't, the rule was living in the part of the window that doesn't survive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Compaction is a good trade — long sessions would be impossible without it. The failure mode is only silent if you haven't decided &lt;em&gt;on purpose&lt;/em&gt; where each rule should live and what gets re-injected when the window turns over. Decide once, wire the hook, and the agent stops forgetting.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I maintain &lt;a href="https://rulestack.gumroad.com?ref=devto" rel="noopener noreferrer"&gt;Rulestack&lt;/a&gt;, a set of rules/skills packs for Claude Code, Cursor, and Codex — including hook recipes like this one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily AI-coding-agent tips on Bluesky: &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt; — or follow our "AI Coding Agents" custom feed there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why your Cursor rules never fire: globs, alwaysApply, and description explained</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Sun, 19 Jul 2026 13:23:52 +0000</pubDate>
      <link>https://dev.to/rulestack/why-your-cursor-rules-never-fire-globs-alwaysapply-and-description-explained-b33</link>
      <guid>https://dev.to/rulestack/why-your-cursor-rules-never-fire-globs-alwaysapply-and-description-explained-b33</guid>
      <description>&lt;p&gt;You wrote a &lt;code&gt;.cursor/rules&lt;/code&gt; file. It has good rules. And the agent keeps ignoring them.&lt;/p&gt;

&lt;p&gt;Nine times out of ten this isn't the model being dumb — it's that the rule never got &lt;strong&gt;attached&lt;/strong&gt; to the context in the first place. Cursor has four ways a rule can enter a chat, and each one is gated by a different frontmatter field. Get the frontmatter wrong and the rule just sits on disk doing nothing.&lt;/p&gt;

&lt;p&gt;Here's the whole model, and the three mistakes that silently switch a rule off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four rule types
&lt;/h2&gt;

&lt;p&gt;A rule in &lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt; becomes one of four types depending on its frontmatter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Fires when&lt;/th&gt;
&lt;th&gt;Frontmatter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Always&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every chat, no matter what&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alwaysApply: true&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto Attached&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An edited or referenced file matches a glob&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;globs:&lt;/code&gt; set, &lt;code&gt;alwaysApply: false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent Requested&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The agent reads the description and decides it's relevant&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;description:&lt;/code&gt; set, no globs, &lt;code&gt;alwaysApply: false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Manual&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only when you &lt;code&gt;@&lt;/code&gt;-mention the rule&lt;/td&gt;
&lt;td&gt;none of the above set&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that last row again: &lt;strong&gt;a rule with no &lt;code&gt;alwaysApply&lt;/code&gt;, no &lt;code&gt;globs&lt;/code&gt;, and no &lt;code&gt;description&lt;/code&gt; is Manual.&lt;/strong&gt; It will never fire on its own. That is the number-one reason a rule "does nothing" — it quietly defaulted to manual and you never @-mentioned it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1: writing &lt;code&gt;globs&lt;/code&gt; as a YAML array
&lt;/h2&gt;

&lt;p&gt;This is the big one. &lt;code&gt;globs&lt;/code&gt; is a &lt;strong&gt;bare, comma-separated string&lt;/strong&gt; — not a YAML list, and not quoted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
globs: src/**/*.ts, src/**/*.tsx
alwaysApply: false
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What people write instead, and what quietly fails to attach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
globs:
  - "src/**/*.ts"        # array form — does not match
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
globs: "src/**/*.ts"     # quoted — does not match
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The glob syntax itself is standard: &lt;code&gt;*&lt;/code&gt; matches one path segment, &lt;code&gt;**&lt;/code&gt; matches any number of directories. &lt;code&gt;src/**/*.tsx&lt;/code&gt;, &lt;code&gt;**/*.ts&lt;/code&gt;, and &lt;code&gt;tailwind.config.*&lt;/code&gt; are all valid. Multiple patterns go on one line separated by commas: &lt;code&gt;docs/**/*.md, docs/**/*.mdx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 2: &lt;code&gt;.md&lt;/code&gt; instead of &lt;code&gt;.mdc&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Rules must use the &lt;code&gt;.mdc&lt;/code&gt; extension. A plain &lt;code&gt;.md&lt;/code&gt; file dropped into &lt;code&gt;.cursor/rules/&lt;/code&gt; is ignored — no error, no warning, it simply isn't a rule. If you pasted a rule out of a README or a gist, check the extension before you check anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 3: an Agent-Requested rule with no description
&lt;/h2&gt;

&lt;p&gt;"Apply Intelligently" rules are the nicest type — the agent pulls them in when they're relevant instead of on a rigid glob. But that decision is made &lt;strong&gt;entirely from the &lt;code&gt;description&lt;/code&gt; field.&lt;/strong&gt; No description means the agent has nothing to match against, so an otherwise Agent-Requested rule collapses back to Manual and never surfaces.&lt;/p&gt;

&lt;p&gt;Write the description as a trigger, not a title:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
description: Conventions for writing and naming Vitest unit tests
alwaysApply: false
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Testing rules" is a title. "Conventions for writing and naming Vitest unit tests" is something the agent can actually match an incoming request against.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check a rule actually attached
&lt;/h2&gt;

&lt;p&gt;Don't guess — look. When you send a message, Cursor surfaces the rules it pulled into that turn as part of the chat's context. If your rule isn't in that list, it didn't fire, and now you know which of the three mistakes to go check. This "did it attach?" loop is the fastest way to debug rules, and it's the exact step most people skip while blaming the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which type should a rule be?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project-wide and non-negotiable&lt;/strong&gt; (never use &lt;code&gt;any&lt;/code&gt;, always keyword arguments): &lt;strong&gt;Always&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack- or folder-specific&lt;/strong&gt; (React component conventions, migration file format): &lt;strong&gt;Auto Attached&lt;/strong&gt; with a glob.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Situational and judgment-heavy&lt;/strong&gt; (how to write tests, how to structure an ADR): &lt;strong&gt;Agent Requested&lt;/strong&gt; with a sharp description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rare, on-demand scaffolding&lt;/strong&gt;: &lt;strong&gt;Manual&lt;/strong&gt;, and @-mention it when you want it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A healthy rules directory is mostly Auto Attached and Agent Requested, with a small handful of Always rules on top. If everything is Always, you're just bloating every prompt; if everything is Manual, nothing ever fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line takeaway
&lt;/h2&gt;

&lt;p&gt;A Cursor rule that "doesn't work" is almost always a rule that never attached. Check the extension (&lt;code&gt;.mdc&lt;/code&gt;), check that &lt;code&gt;globs&lt;/code&gt; is a bare comma-separated string, and give Agent-Requested rules a real description — then watch the context list to confirm it fired before you touch the rule's actual content.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Maintaining a full &lt;code&gt;.cursor/rules&lt;/code&gt; set — plus the Claude Code and Codex equivalents — by hand is a lot. That's what I build at &lt;a href="https://rulestack.gumroad.com?ref=devto" rel="noopener noreferrer"&gt;Rulestack&lt;/a&gt;: production-ready rule packs for Cursor, Claude Code, and Codex.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I also post short AI-coding-agent tips on Bluesky — follow along at &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>MCP servers in Claude Code: the minimal setup, and when you should skip MCP entirely</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Sat, 18 Jul 2026 22:23:24 +0000</pubDate>
      <link>https://dev.to/rulestack/mcp-servers-in-claude-code-the-minimal-setup-and-when-you-should-skip-mcp-entirely-1499</link>
      <guid>https://dev.to/rulestack/mcp-servers-in-claude-code-the-minimal-setup-and-when-you-should-skip-mcp-entirely-1499</guid>
      <description>&lt;p&gt;Most MCP setup guides start with a wall of JSON. You don't need it. Claude Code can register an MCP server with one CLI command, and for a lot of teams the right number of MCP servers is zero — a shell command in a rules file does the same job with less context overhead. This is the minimal path, the config that actually gets written to disk, and the handful of failure modes that account for almost every "my server doesn't show up" thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-command happy path
&lt;/h2&gt;

&lt;p&gt;For a local (stdio) server, this is the whole setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add github &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @modelcontextprotocol/server-github
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inside a session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/mcp&lt;/code&gt; shows every configured server, its connection state, and the tools it exposes. If your server is listed and connected, you're done. If it isn't, skip to the failure modes section — the cause is almost always one of four things.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--&lt;/code&gt; separator matters: everything after it is the command Claude Code runs to start the server process. Flags before it belong to &lt;code&gt;claude mcp add&lt;/code&gt; itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scopes: where the config actually lives
&lt;/h2&gt;

&lt;p&gt;Every MCP server has a scope, and the scope decides which file it's written to and who else gets it. This is the part most people discover by accident when a server follows them into an unrelated project — or doesn't follow them into the one where they wanted it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Written to&lt;/th&gt;
&lt;th&gt;Who sees it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;local (default)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;your user config, keyed to the current project&lt;/td&gt;
&lt;td&gt;just you, just this project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--scope project&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.mcp.json&lt;/code&gt; at the repo root&lt;/td&gt;
&lt;td&gt;everyone who clones the repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--scope user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your user config, global&lt;/td&gt;
&lt;td&gt;just you, every project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two practical rules fall out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A server your whole team needs belongs in &lt;code&gt;.mcp.json&lt;/code&gt;&lt;/strong&gt;, committed like any other config file. Teammates get it on pull. Claude Code asks each person to approve a project's servers on first use (they're arbitrary executables from the internet's point of view), so nobody silently runs what you committed. If you decline the prompt and change your mind later, &lt;code&gt;claude mcp reset-project-choices&lt;/code&gt; re-asks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A server for your personal workflow belongs in user scope&lt;/strong&gt;, not copy-pasted into every project's local scope. &lt;code&gt;claude mcp add --scope user notion ...&lt;/code&gt; once, and it's everywhere.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;.mcp.json&lt;/code&gt; is plain JSON and fine to edit by hand:&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;"mcpServers"&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;"github"&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;"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;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@modelcontextprotocol/server-github"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"GITHUB_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${GITHUB_TOKEN}"&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;That &lt;code&gt;${GITHUB_TOKEN}&lt;/code&gt; is not decoration — Claude Code expands environment variable references in &lt;code&gt;.mcp.json&lt;/code&gt; (&lt;code&gt;${VAR}&lt;/code&gt;, with &lt;code&gt;${VAR:-fallback}&lt;/code&gt; for defaults) at server start. This is how you commit a shared config without committing anyone's token: the file references the variable, each machine supplies its own value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote servers
&lt;/h2&gt;

&lt;p&gt;Servers that live behind a URL instead of a local process use a transport flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http sentry https://mcp.sentry.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For services that need auth, run &lt;code&gt;/mcp&lt;/code&gt; in a session and pick the server — Claude Code walks the OAuth flow in your browser and stores the token. No key ends up in a config file, which is exactly where keys shouldn't be.&lt;/p&gt;

&lt;p&gt;Remote HTTP servers have one operational property worth internalizing: they fail with the network. If a provider's MCP endpoint is down, your session just loses those tools until it recovers. Design your rules so that nothing critical assumes an MCP tool is always present.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four failure modes that actually happen
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The command isn't on PATH where Claude Code runs.&lt;/strong&gt; Your shell has nvm-managed node; the environment Claude Code launched from may not. If &lt;code&gt;/mcp&lt;/code&gt; shows the server as failed and the log says &lt;code&gt;command not found&lt;/code&gt;, use an absolute path to the binary (&lt;code&gt;command: "/Users/you/.nvm/versions/node/v22.17.0/bin/npx"&lt;/code&gt;) or a wrapper script that sources your environment first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The env block is missing.&lt;/strong&gt; A stdio server is a child process — it inherits nothing you didn't give it. If the server needs &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; and the &lt;code&gt;env&lt;/code&gt; block doesn't pass it, the server usually starts fine and then every tool call fails with an auth error. The confusing symptom: connected server, broken tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The server is slow to start and gets killed.&lt;/strong&gt; Claude Code enforces a startup timeout (configurable with the &lt;code&gt;MCP_TIMEOUT&lt;/code&gt; environment variable). First-run &lt;code&gt;npx&lt;/code&gt; downloads are the classic trigger: the download takes longer than the timeout, the server "fails", and the second attempt mysteriously works because the package is now cached. Warm the cache (&lt;code&gt;npx -y &amp;lt;pkg&amp;gt; --help&lt;/code&gt; once) or raise the timeout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. It's a scope problem, not a server problem.&lt;/strong&gt; The server works in the project where you added it and is "missing" everywhere else — because default scope is local. &lt;code&gt;claude mcp list&lt;/code&gt; shows what's configured in the current context; if the server isn't in the list, it's not broken, it's just somewhere else.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;claude --debug&lt;/code&gt; when none of the above fits — it logs each server's startup and the actual error instead of a silent absence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permissions: MCP tools are just tools
&lt;/h2&gt;

&lt;p&gt;Every MCP tool gets a name of the form &lt;code&gt;mcp__&amp;lt;server&amp;gt;__&amp;lt;tool&amp;gt;&lt;/code&gt;, and your permission rules treat it like any other tool. Two useful patterns:&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="s2"&gt;"mcp__github__search_repositories"&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="s2"&gt;"mcp__github__delete_repository"&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;&lt;code&gt;mcp__github&lt;/code&gt; (no tool suffix) matches every tool the server exposes. One thing to know: MCP tool rules don't take argument patterns — you can allow or deny a tool, but not "this tool with these arguments". Scope your write-capable tokens accordingly: the permission system controls which tools run, the token controls what they can touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you don't need MCP at all
&lt;/h2&gt;

&lt;p&gt;Every connected server injects its tool definitions into context — that's capability you pay for in tokens on every request, whether or not the session uses it. Before adding a server, it's worth asking what the integration is actually for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrapping a CLI you already have?&lt;/strong&gt; Claude Code can run &lt;code&gt;gh&lt;/code&gt;, &lt;code&gt;aws&lt;/code&gt;, &lt;code&gt;kubectl&lt;/code&gt;, &lt;code&gt;psql&lt;/code&gt; through Bash directly. A line in your rules file ("use &lt;code&gt;gh&lt;/code&gt; for GitHub operations") costs a few tokens once. An MCP server that duplicates a CLI costs schema tokens forever and adds a process to babysit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-off API calls?&lt;/strong&gt; A documented &lt;code&gt;curl&lt;/code&gt; pattern in a rules file is transparent, debuggable, and versioned with your repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Genuinely stateful integrations&lt;/strong&gt; — OAuth-gated services with no usable CLI, streaming data sources, tools that need server-side session state — are what MCP is for. Sentry, Notion, browser control, internal platform APIs behind SSO: real use cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The heuristic that has held up: &lt;strong&gt;if you can express it as a command, write it as a rule; if it needs a connection, make it a server.&lt;/strong&gt; Teams that follow it end up with one to three servers doing work nothing else can do, instead of eight servers reimplementing their shell.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Rulestack builds config packs for Claude Code, Cursor, and Codex — rules files, hooks, and permission setups tested against current tool behavior: &lt;a href="https://rulestack.gumroad.com?ref=devto?ref=devto" rel="noopener noreferrer"&gt;https://rulestack.gumroad.com?ref=devto?ref=devto&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Short, practical notes on AI coding agents, most days, on Bluesky: &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>claude</category>
    </item>
    <item>
      <title>Claude Code permission rules: how allow, deny, and ask actually match</title>
      <dc:creator>Rulestack</dc:creator>
      <pubDate>Fri, 17 Jul 2026 15:41:36 +0000</pubDate>
      <link>https://dev.to/rulestack/claude-code-permission-rules-how-allow-deny-and-ask-actually-match-1bj7</link>
      <guid>https://dev.to/rulestack/claude-code-permission-rules-how-allow-deny-and-ask-actually-match-1bj7</guid>
      <description>&lt;p&gt;Claude Code's permission system is small enough to fit in one settings file and subtle enough that most people write at least one rule that does nothing. The rule looks correct, the JSON parses, Claude Code starts, and the rule is quietly never consulted. This is a walk through how allow/deny/ask actually match, where the files live, and the specifiers that look plausible but are silently ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the rules live
&lt;/h2&gt;

&lt;p&gt;Permission rules go in the &lt;code&gt;permissions&lt;/code&gt; key of a &lt;code&gt;settings.json&lt;/code&gt;. There are four scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;~/.claude/settings.json&lt;/code&gt; — your user settings, applied in every project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/settings.json&lt;/code&gt; — project settings, checked into source control and shared with the team.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.claude/settings.local.json&lt;/code&gt; — your personal project overrides. When Claude Code creates this file, it configures git to ignore it.&lt;/li&gt;
&lt;li&gt;Enterprise managed settings — deployed by an administrator and not overridable. The path is OS-specific: &lt;code&gt;/Library/Application Support/ClaudeCode/managed-settings.json&lt;/code&gt; on macOS, &lt;code&gt;/etc/claude-code/managed-settings.json&lt;/code&gt; on Linux and WSL, and &lt;code&gt;C:\Program Files\ClaudeCode\managed-settings.json&lt;/code&gt; on Windows. All three platforms also read a &lt;code&gt;managed-settings.d/&lt;/code&gt; drop-in directory beside that file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Precedence, highest to lowest: &lt;strong&gt;managed → command-line arguments → local → project → user.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is one thing that trips people up here. Most settings &lt;em&gt;override&lt;/em&gt; down the chain — a project value replaces a user value. Permission rules do not. The &lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;deny&lt;/code&gt;, and &lt;code&gt;ask&lt;/code&gt; lists &lt;strong&gt;merge&lt;/strong&gt; across every scope. Your user &lt;code&gt;deny&lt;/code&gt; rules and the project &lt;code&gt;deny&lt;/code&gt; rules are both in force at once. The practical consequence: &lt;strong&gt;if a tool is denied at any level, no other level can allow it.&lt;/strong&gt; A user-level deny beats a project-level allow, and a managed deny beats everything, including &lt;code&gt;--allowedTools&lt;/code&gt; on the command line.&lt;/p&gt;

&lt;h2&gt;
  
  
  allow, deny, ask — and how deny wins
&lt;/h2&gt;

&lt;p&gt;Three lists, three behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;allow&lt;/strong&gt; — use the tool without a prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ask&lt;/strong&gt; — always prompt for confirmation, even if something else would have allowed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;deny&lt;/strong&gt; — refuse the call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rules are evaluated in a fixed order: &lt;strong&gt;deny, then ask, then allow. The first match in that order wins, and specificity does not change the order.&lt;/strong&gt; This is the opposite of how CSS or firewall rules often work, and it is the single most important thing to internalize.&lt;/p&gt;

&lt;p&gt;A broad deny cannot carry allowlist exceptions. If you write:&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;"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="s2"&gt;"Bash(aws *)"&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="s2"&gt;"Bash(aws s3 ls)"&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;then &lt;code&gt;aws s3 ls&lt;/code&gt; is blocked. The deny matched first; the more specific allow never gets a turn. The same applies between ask and allow — a matching &lt;code&gt;ask&lt;/code&gt; rule prompts even when a narrower &lt;code&gt;allow&lt;/code&gt; also matches.&lt;/p&gt;

&lt;p&gt;One more distinction worth knowing: a bare tool-name deny and a scoped deny behave differently. &lt;code&gt;"deny": ["Bash"]&lt;/code&gt; removes Bash from Claude's context entirely — the model never sees the tool. &lt;code&gt;"deny": ["Bash(rm *)"]&lt;/code&gt; leaves Bash available and blocks only matching calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool specifier syntax
&lt;/h2&gt;

&lt;p&gt;A rule is either &lt;code&gt;Tool&lt;/code&gt; or &lt;code&gt;Tool(specifier)&lt;/code&gt;. A bare tool name matches every use: &lt;code&gt;Bash&lt;/code&gt;, &lt;code&gt;WebFetch&lt;/code&gt;, &lt;code&gt;Read&lt;/code&gt;. &lt;code&gt;Bash(*)&lt;/code&gt; is equivalent to &lt;code&gt;Bash&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bash
&lt;/h3&gt;

&lt;p&gt;Bash specifiers are glob patterns, and the wildcard can sit anywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Bash(npm run build)&lt;/code&gt; — the exact command.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Bash(npm run test:*)&lt;/code&gt; — a prefix match. The &lt;code&gt;:*&lt;/code&gt; suffix is Claude Code's canonical way to write a trailing wildcard; it is exactly equivalent to &lt;code&gt;Bash(npm run test *)&lt;/code&gt;. Note &lt;code&gt;:*&lt;/code&gt; is only recognized at the &lt;em&gt;end&lt;/em&gt; of a pattern.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Bash(git * main)&lt;/code&gt; — wildcards mid-pattern; matches &lt;code&gt;git checkout main&lt;/code&gt; and &lt;code&gt;git merge main&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The space before a trailing &lt;code&gt;*&lt;/code&gt; is load-bearing. &lt;code&gt;Bash(ls *)&lt;/code&gt; matches &lt;code&gt;ls -la&lt;/code&gt; but &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;lsof&lt;/code&gt;, because the space enforces a word boundary. &lt;code&gt;Bash(ls*)&lt;/code&gt; with no space matches both.&lt;/p&gt;

&lt;p&gt;Here is where a widely repeated claim is wrong. Prefix matching &lt;strong&gt;is&lt;/strong&gt; shell-operator aware. A rule like &lt;code&gt;Bash(safe-cmd *)&lt;/code&gt; will &lt;em&gt;not&lt;/em&gt; authorize &lt;code&gt;safe-cmd &amp;amp;&amp;amp; other-cmd&lt;/code&gt;. Claude Code splits compound commands on &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;||&lt;/code&gt;, &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;|&amp;amp;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, and newlines, and every subcommand must match a rule on its own. So the danger is not that operators leak through — they don't.&lt;/p&gt;

&lt;p&gt;The real fragility is in trying to constrain &lt;em&gt;arguments&lt;/em&gt;. The docs are explicit that a pattern like &lt;code&gt;Bash(curl http://github.com/ *)&lt;/code&gt; fails to do what it looks like it does. It won't match &lt;code&gt;curl -X GET http://github.com/...&lt;/code&gt; (option before the URL), &lt;code&gt;curl https://github.com/...&lt;/code&gt; (different protocol), &lt;code&gt;curl -L http://bit.ly/xyz&lt;/code&gt; (redirects to GitHub), &lt;code&gt;URL=http://github.com &amp;amp;&amp;amp; curl $URL&lt;/code&gt; (variable), or &lt;code&gt;curl  http://github.com&lt;/code&gt; (extra space). If you want to restrict network destinations, deny &lt;code&gt;curl&lt;/code&gt;/&lt;code&gt;wget&lt;/code&gt; and use &lt;code&gt;WebFetch(domain:...)&lt;/code&gt; instead, or enforce it in a &lt;code&gt;PreToolUse&lt;/code&gt; hook. A Bash argument pattern is guidance, not a boundary.&lt;/p&gt;

&lt;p&gt;One convenience: Claude Code strips a fixed set of process wrappers before matching, so &lt;code&gt;Bash(npm test *)&lt;/code&gt; also covers &lt;code&gt;timeout 30 npm test&lt;/code&gt;. The stripped wrappers are &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;nice&lt;/code&gt;, &lt;code&gt;nohup&lt;/code&gt;, &lt;code&gt;stdbuf&lt;/code&gt;, and unflagged &lt;code&gt;xargs&lt;/code&gt;. Environment runners like &lt;code&gt;npx&lt;/code&gt;, &lt;code&gt;docker exec&lt;/code&gt;, and &lt;code&gt;devbox run&lt;/code&gt; are &lt;strong&gt;not&lt;/strong&gt; stripped — &lt;code&gt;Bash(devbox run *)&lt;/code&gt; would match &lt;code&gt;devbox run rm -rf .&lt;/code&gt;, so write the full inner command instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read and Edit paths
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Edit&lt;/code&gt; rules apply to every built-in file-editing tool, not just the Edit tool. &lt;code&gt;Read&lt;/code&gt; rules apply best-effort to Read, Grep, Glob, &lt;code&gt;@file&lt;/code&gt; mentions, and IDE-shared context.&lt;/p&gt;

&lt;p&gt;Path patterns follow gitignore semantics, with four anchor types that are easy to confuse:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Anchored at&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;//path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;filesystem root&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Read(//Users/alice/secrets/**)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;home directory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Read(~/.zshrc)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/path&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the settings source&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Edit(/src/**/*.ts)&lt;/code&gt; → &lt;code&gt;&amp;lt;project root&amp;gt;/src/...&lt;/code&gt; in project settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;path&lt;/code&gt; or &lt;code&gt;./path&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;current directory&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Read(.env)&lt;/code&gt; → &lt;code&gt;&amp;lt;cwd&amp;gt;/.env&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trap: &lt;strong&gt;a single leading slash is not an absolute path.&lt;/strong&gt; &lt;code&gt;Read(/secrets/**)&lt;/code&gt; in your user settings matches &lt;code&gt;~/.claude/secrets/**&lt;/code&gt;, not &lt;code&gt;/secrets&lt;/code&gt; and not your project. For a genuine absolute path use &lt;code&gt;//&lt;/code&gt;. For a rule in user settings that should apply inside every project, use &lt;code&gt;//&lt;/code&gt; or &lt;code&gt;~/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Bare filenames match at any depth, so &lt;code&gt;Read(.env)&lt;/code&gt; and &lt;code&gt;Read(**/.env)&lt;/code&gt; are equivalent and both cover &lt;code&gt;config/.env&lt;/code&gt;. As of v2.1.208 a &lt;code&gt;Read&lt;/code&gt; deny rule also blocks the Edit tool on the same path — but not Write or NotebookEdit, so add an explicit &lt;code&gt;Edit(...)&lt;/code&gt; deny for paths nothing should change.&lt;/p&gt;

&lt;h3&gt;
  
  
  WebFetch and MCP
&lt;/h3&gt;

&lt;p&gt;WebFetch matches on hostname, case-insensitively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WebFetch(domain:example.com)&lt;/code&gt; — that host.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WebFetch(domain:*.example.com)&lt;/code&gt; — any subdomain (&lt;code&gt;api.example.com&lt;/code&gt;), but &lt;strong&gt;not&lt;/strong&gt; the apex &lt;code&gt;example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WebFetch(domain:*)&lt;/code&gt; — equivalent to a bare &lt;code&gt;WebFetch&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MCP rules use the server name, optionally with a tool name. Contrary to another common belief, the tool-level wildcard is supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mcp__puppeteer&lt;/code&gt; — every tool from the &lt;code&gt;puppeteer&lt;/code&gt; server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcp__puppeteer__*&lt;/code&gt; — also every tool from that server (explicit wildcard).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcp__puppeteer__puppeteer_navigate&lt;/code&gt; — one specific tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch for &lt;code&gt;allow&lt;/code&gt; rules: a tool-name glob is only honored after a literal &lt;code&gt;mcp__&amp;lt;server&amp;gt;__&lt;/code&gt; prefix, and the server segment itself must be glob-free. &lt;code&gt;mcp__github__get_*&lt;/code&gt; is fine; an unanchored &lt;code&gt;mcp__*&lt;/code&gt; in an &lt;code&gt;allow&lt;/code&gt; list is skipped with a warning and approves nothing. (&lt;code&gt;mcp__*&lt;/code&gt; works fine as a &lt;em&gt;deny&lt;/em&gt; rule.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameter matching
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;deny&lt;/code&gt; and &lt;code&gt;ask&lt;/code&gt; rules can match a top-level input parameter with &lt;code&gt;Tool(param:value)&lt;/code&gt; — &lt;code&gt;Agent(model:opus)&lt;/code&gt;, &lt;code&gt;Bash(run_in_background:true)&lt;/code&gt;. The value supports &lt;code&gt;*&lt;/code&gt;. This does &lt;strong&gt;not&lt;/strong&gt; work for fields a tool already canonicalizes: &lt;code&gt;command&lt;/code&gt; (Bash), &lt;code&gt;file_path&lt;/code&gt; (Read/Edit/Write), &lt;code&gt;path&lt;/code&gt; (Grep/Glob), &lt;code&gt;notebook_path&lt;/code&gt; (NotebookEdit), and &lt;code&gt;url&lt;/code&gt; (WebFetch). Which leads directly to the most valuable section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules that silently do nothing
&lt;/h2&gt;

&lt;p&gt;These parse fine and are then ignored. Several now emit a startup warning, but only on recent versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Write(path)&lt;/code&gt;, &lt;code&gt;NotebookEdit(path)&lt;/code&gt;, &lt;code&gt;Glob(path)&lt;/code&gt;.&lt;/strong&gt; As of v2.1.210, the file-permission checks only match &lt;code&gt;Edit(path)&lt;/code&gt; and &lt;code&gt;Read(path)&lt;/code&gt; rules. A path rule on Write, NotebookEdit, or Glob is accepted and never consulted, and Claude Code now warns at startup for each one. Use &lt;code&gt;Edit(...)&lt;/code&gt; in place of &lt;code&gt;Write(...)&lt;/code&gt; or &lt;code&gt;NotebookEdit(...)&lt;/code&gt; (Edit rules cover all file-editing tools), and &lt;code&gt;Read(...)&lt;/code&gt; in place of &lt;code&gt;Glob(...)&lt;/code&gt;. The exact warning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Permission deny rule (.claude/settings.json): Write(docs/**) is not matched by file permission checks — only Edit(path) rules are. Use Edit(docs/**) instead (Edit rules cover all file-editing tools).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare &lt;code&gt;Write&lt;/code&gt; deny with no path is unaffected — it removes the tool everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Bash(command:rm *)&lt;/code&gt; and other canonicalized-field param rules.&lt;/strong&gt; Because &lt;code&gt;command&lt;/code&gt; is a field Bash already matches with its own rules, &lt;code&gt;Bash(command:...)&lt;/code&gt; is ignored and warned about. Write &lt;code&gt;Bash(rm *)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unanchored allow globs.&lt;/strong&gt; &lt;code&gt;"*"&lt;/code&gt;, &lt;code&gt;"B*"&lt;/code&gt;, or &lt;code&gt;mcp__*&lt;/code&gt; in the &lt;code&gt;allow&lt;/code&gt; list are skipped with a warning and auto-approve nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Typo'd or mislabeled tool names.&lt;/strong&gt; A deny/ask rule whose tool name matches no known tool warns at startup (names containing &lt;code&gt;_&lt;/code&gt; or &lt;code&gt;*&lt;/code&gt; are exempt). Also, the label you see in the transcript is not always the canonical name — the tool shown as &lt;code&gt;Stop Task&lt;/code&gt; is canonically &lt;code&gt;TaskStop&lt;/code&gt;, and a rule written &lt;code&gt;Stop Task&lt;/code&gt; will not match. Use the canonical names from the tools reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefix rules on exec wrappers.&lt;/strong&gt; &lt;code&gt;watch&lt;/code&gt;, &lt;code&gt;setsid&lt;/code&gt;, &lt;code&gt;ionice&lt;/code&gt;, and &lt;code&gt;flock&lt;/code&gt; always prompt — a &lt;code&gt;Bash(watch *)&lt;/code&gt; allow rule can't auto-approve them. The same goes for &lt;code&gt;find&lt;/code&gt; with &lt;code&gt;-exec&lt;/code&gt; or &lt;code&gt;-delete&lt;/code&gt;: &lt;code&gt;Bash(find *)&lt;/code&gt; doesn't cover those forms, because the glob could smuggle in an exec. If you need one of these, write an exact-match rule for the full command string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;/permissions&lt;/code&gt; in a session to see every active rule and which &lt;code&gt;settings.json&lt;/code&gt; file each one came from — the fastest way to find a rule that isn't taking effect because a higher scope shadows it. Start with &lt;code&gt;--verbose&lt;/code&gt; to see the exact parameter names and values Claude Code sees for each tool call, which is what you need when a param-match rule isn't firing.&lt;/p&gt;

&lt;p&gt;Modes set the baseline that rules layer on top of. There are more than the usual three: &lt;code&gt;default&lt;/code&gt; (labeled Manual; prompts on first use), &lt;code&gt;acceptEdits&lt;/code&gt; (auto-accepts edits and common filesystem commands in your working directory), &lt;code&gt;plan&lt;/code&gt; (read-only exploration, no edits), &lt;code&gt;auto&lt;/code&gt; (auto-approves with a background safety classifier), &lt;code&gt;dontAsk&lt;/code&gt; (auto-denies anything not pre-approved — for CI), and &lt;code&gt;bypassPermissions&lt;/code&gt; (skips prompts entirely; for throwaway containers only). In the CLI, &lt;code&gt;Shift+Tab&lt;/code&gt; cycles between permission modes. Set a persistent default with &lt;code&gt;defaultMode&lt;/code&gt; under &lt;code&gt;permissions&lt;/code&gt;. Deny and explicit ask rules still apply in every mode, including &lt;code&gt;bypassPermissions&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A starter settings.json
&lt;/h2&gt;

&lt;p&gt;A conservative baseline for a TypeScript repo. Every form here is a verified, matched specifier:&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;"defaultMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&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(npm run build)"&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 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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(npx tsc:*)"&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 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="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;"ask"&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 push *)"&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;"Edit(.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;"Bash(curl *)"&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(wget *)"&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 &lt;code&gt;Read(.env)&lt;/code&gt; and matching &lt;code&gt;Edit(.env)&lt;/code&gt; pair keeps both reading and writing your secrets out of reach; the &lt;code&gt;Bash(curl *)&lt;/code&gt;/&lt;code&gt;Bash(wget *)&lt;/code&gt; denies close the network side-channel that a Read deny alone leaves open. Keep this in &lt;code&gt;.claude/settings.json&lt;/code&gt; for team-wide rules, and put anything personal in &lt;code&gt;.claude/settings.local.json&lt;/code&gt;, remembering that both lists merge and deny always wins.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Rulestack publishes production-tested rules and config packs for Claude Code, Cursor, and Codex — including ready-to-use settings and hooks setups: &lt;a href="https://rulestack.gumroad.com?ref=devto" rel="noopener noreferrer"&gt;https://rulestack.gumroad.com?ref=devto&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily tips on AI coding agents on Bluesky: follow &lt;a href="https://bsky.app/profile/ai-shop.bsky.social" rel="noopener noreferrer"&gt;@ai-shop.bsky.social&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devtools</category>
      <category>productivity</category>
      <category>claude</category>
    </item>
  </channel>
</rss>
