<?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: Hamid Ahmadian</title>
    <description>The latest articles on DEV Community by Hamid Ahmadian (@hamid_ahmadian_3570449f72).</description>
    <link>https://dev.to/hamid_ahmadian_3570449f72</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%2F4087221%2F7610dc14-30a5-4b1c-ab7b-f97d4d038645.jpg</url>
      <title>DEV Community: Hamid Ahmadian</title>
      <link>https://dev.to/hamid_ahmadian_3570449f72</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hamid_ahmadian_3570449f72"/>
    <language>en</language>
    <item>
      <title>Claude Code Permission Rules and Hooks: The Precedence Order Nobody Documents Clearly</title>
      <dc:creator>Hamid Ahmadian</dc:creator>
      <pubDate>Wed, 09 Sep 2026 07:35:20 +0000</pubDate>
      <link>https://dev.to/hamid_ahmadian_3570449f72/claude-code-permission-rules-and-hooks-the-precedence-order-nobody-documents-clearly-3jmg</link>
      <guid>https://dev.to/hamid_ahmadian_3570449f72/claude-code-permission-rules-and-hooks-the-precedence-order-nobody-documents-clearly-3jmg</guid>
      <description>&lt;p&gt;Most write-ups on Claude Code's permission system stop at "there are allow, deny, and ask rules." That's true, but it skips the part that actually determines whether your setup holds up under pressure: the rules have a fixed evaluation order, hooks interact with that order in a specific and non-obvious way, and a small set of paths can't be unlocked by any rule at all, in any mode short of full bypass. Here's the part that's easy to get wrong in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deny beats allow, always — specificity never matters
&lt;/h2&gt;

&lt;p&gt;Claude Code evaluates deny, then ask, then allow, and the first match wins. Rule specificity does not change the outcome. A broad &lt;code&gt;Bash(aws *)&lt;/code&gt; deny blocks a call even when a much narrower &lt;code&gt;Bash(aws s3 ls)&lt;/code&gt; allow also matches. This is the single most common source of "why isn't my allow rule working" confusion — if a broader deny exists anywhere in the chain, no allow rule, however precisely scoped, can punch a hole in it. If you're debugging a rule that seems to be ignored, check for a deny match before you assume the allow syntax is wrong.&lt;/p&gt;

&lt;p&gt;There's also a distinction worth internalizing between a bare tool-name deny and a scoped one. &lt;code&gt;"deny": ["Bash"]&lt;/code&gt; removes Bash from Claude's available tools entirely — it's not an option Claude even considers. &lt;code&gt;Bash(rm *)&lt;/code&gt; leaves Bash available but blocks matching invocations when attempted. Deciding whether to eliminate a capability outright or fence off a dangerous subset of it is a real design choice, not just a syntax preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few paths nothing can unlock
&lt;/h2&gt;

&lt;p&gt;Two categories are protected below the level where rules even get consulted. &lt;strong&gt;Protected paths&lt;/strong&gt; — &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;.claude&lt;/code&gt;, &lt;code&gt;.vscode&lt;/code&gt;, shell rc files, &lt;code&gt;.mcp.json&lt;/code&gt;, and similar — are never auto-approved by any allow rule, in any mode short of &lt;code&gt;bypassPermissions&lt;/code&gt;. The safety check runs &lt;em&gt;before&lt;/em&gt; Claude Code evaluates your allow rules, so an entry like &lt;code&gt;Edit(.claude/**)&lt;/code&gt; in &lt;code&gt;settings.json&lt;/code&gt; has no effect on this gate. That's deliberate: it stops a compromised or misconfigured allow rule from silently granting write access to the files that define Claude Code's own permissions and hooks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical paths&lt;/strong&gt; are the same idea applied to destructive removal: &lt;code&gt;rm&lt;/code&gt;/&lt;code&gt;rmdir&lt;/code&gt; targeting the filesystem root, a top-level directory like &lt;code&gt;/usr&lt;/code&gt; or &lt;code&gt;/etc&lt;/code&gt;, your home directory, or your working directory and its parents is refused — no allow rule and no &lt;code&gt;PreToolUse&lt;/code&gt; hook returning &lt;code&gt;"allow"&lt;/code&gt; can approve one of these, in any mode, including bypass. It's a hard-coded circuit breaker against model error, not a policy setting you can tune.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks are dynamic; rules are static — and they interact in one specific way
&lt;/h2&gt;

&lt;p&gt;Permission rules are a fixed list you write once. &lt;code&gt;PreToolUse&lt;/code&gt; hooks are scripts that run before a tool call executes, inspect the actual arguments, and return a decision based on runtime context a static rule can't express — scanning a file's contents, calling a validation service, checking what a command would actually touch.&lt;/p&gt;

&lt;p&gt;The interaction is worth memorizing exactly: hook decisions do not bypass permission rules. Claude Code still evaluates deny and ask rules regardless of what a &lt;code&gt;PreToolUse&lt;/code&gt; hook returns — a matching deny rule blocks a call even if the hook said &lt;code&gt;"allow"&lt;/code&gt;. The reverse also holds: a blocking hook (exit code 2) overrides allow rules. That gives you a genuinely useful pattern — allow a whole tool broadly (&lt;code&gt;"allow": ["Bash"]&lt;/code&gt;) and register a hook that rejects only the specific commands you actually want blocked, instead of hand-maintaining an exhaustive deny list that has to anticipate every dangerous variant in advance:&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;# .claude/hooks/block-rm.sh&lt;/span&gt;
&lt;span class="nv"&gt;COMMAND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&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;echo&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;-q&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'{hookSpecificOutput:{hookEventName:"PreToolUse",
    permissionDecision:"deny",
    permissionDecisionReason:"Destructive command blocked by hook"}}'&lt;/span&gt;
&lt;span class="k"&gt;fi
&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;Exit code 2 is a blocking error — it prevents the tool call outright, even if the hook also printed JSON saying &lt;code&gt;"permissionDecision": "allow"&lt;/code&gt;. Any other non-zero exit is treated as non-blocking: the call proceeds through the normal permission flow as though the hook hadn't run at all. That distinction between "exit 2" and "any other failure" is easy to get backwards under a deadline, and getting it backwards means a hook you wrote to block something silently does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule of thumb
&lt;/h2&gt;

&lt;p&gt;Use permission rules for static, unconditional facts: "never let Claude read &lt;code&gt;secrets/&lt;/code&gt;" doesn't depend on context, so a &lt;code&gt;deny&lt;/code&gt; rule is the right tool. Use a hook for anything that depends on inspecting what a command actually contains: "block any commit that touches the production migration path" needs runtime context a name-matched rule can't express. Deterministic controls — deny rules, a hook that reliably exits 2 on a match — hold every time, regardless of how a specific request got reasoned about. A classifier or a well-behaved model only reduces how often something goes wrong; it isn't a guarantee.&lt;/p&gt;

&lt;p&gt;None of this replaces sandboxing, and it isn't the whole threat model — prompt injection and MCP trust are their own topics. But precedence order, protected/critical paths, and the hook-vs-rule interaction are the three things that most often produce a security policy that looks correct on paper and doesn't actually hold. I wrote up the fuller picture — sandbox scope, MCP trust, managed settings, incident response — at &lt;a href="https://omniatlas.ai/subjects/claude-code-sandboxing-and-security?utm_source=devto" rel="noopener noreferrer"&gt;Claude Code Sandboxing and Security&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>claude</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>How Claude Code Actually Works: The Agentic Loop, Not Autocomplete</title>
      <dc:creator>Hamid Ahmadian</dc:creator>
      <pubDate>Thu, 20 Aug 2026 21:34:42 +0000</pubDate>
      <link>https://dev.to/hamid_ahmadian_3570449f72/how-claude-code-actually-works-the-agentic-loop-not-autocomplete-2m4</link>
      <guid>https://dev.to/hamid_ahmadian_3570449f72/how-claude-code-actually-works-the-agentic-loop-not-autocomplete-2m4</guid>
      <description>&lt;p&gt;Autocomplete predicts the next few tokens from the file you're looking at. Claude Code does something structurally different: it runs a loop — decide what to do, do it with a real tool, look at what actually happened, decide what to do next — until the task is done or it needs you. Here's what that loop actually looks like, tool call by tool call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Autocomplete vs. an agent
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Inline autocomplete&lt;/th&gt;
&lt;th&gt;Claude Code (agentic)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Input&lt;/td&gt;
&lt;td&gt;Current file, cursor position&lt;/td&gt;
&lt;td&gt;Your request, plus whatever it chooses to read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action&lt;/td&gt;
&lt;td&gt;Predicts next tokens&lt;/td&gt;
&lt;td&gt;Chooses and executes tools: read, search, edit, run commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feedback&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Sees command output, test results, errors — and reacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;One file, one cursor&lt;/td&gt;
&lt;td&gt;Any number of files, your terminal, your git history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stops when&lt;/td&gt;
&lt;td&gt;It emits a suggestion&lt;/td&gt;
&lt;td&gt;The task is verified done, or it needs your input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The difference that matters is the feedback loop. Autocomplete has no way of knowing if the code it suggested even compiles. Claude Code can run &lt;code&gt;pytest&lt;/code&gt;, see a failure, read the traceback, fix the function, and re-run the tests — without you doing anything in between.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop, concretely
&lt;/h2&gt;

&lt;p&gt;Every task cycles through three phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gather context&lt;/strong&gt; — read a file, grep for a symbol, check test output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take action&lt;/strong&gt; — edit a file, run a command, stage a commit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; — did the edit apply, did the tests pass, did the command exit 0&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Given "fix the failing tests," a plausible sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Bash    -&amp;gt; run the test suite, see what fails
2. Read    -&amp;gt; open the file the failing test exercises
3. Grep    -&amp;gt; find other callers of the broken function
4. Edit    -&amp;gt; apply the fix
5. Bash    -&amp;gt; re-run the tests
   still failing?  -&amp;gt; back to step 2
   passing?        -&amp;gt; report what was wrong and what changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here is scripted — it's what emerges from feeding each tool result back into the next decision. That's the mechanical definition of "agentic": the next action is a function of the last observation, not a fixed script.&lt;/p&gt;

&lt;p&gt;Two details explain behavior that otherwise looks mysterious. &lt;strong&gt;Edit requires a prior Read&lt;/strong&gt; — it won't blindly rewrite a file it hasn't looked at this conversation. And &lt;strong&gt;Edit is exact-string replacement&lt;/strong&gt;, not a diff or regex — if the snippet it's trying to match isn't verbatim present, the edit fails outright rather than silently applying something close-enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permission modes: the safety/autonomy dial
&lt;/h2&gt;

&lt;p&gt;Handing a model shell access is powerful and genuinely risky, so Claude Code gates it with permission modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto&lt;/strong&gt; — reads, edits, and shell commands run without prompting; a separate classifier model reviews actions in the background and blocks risky categories (force-pushes, prod deploys, &lt;code&gt;curl | bash&lt;/code&gt;). Default on Pro/Max/Team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual&lt;/strong&gt; — asks before every edit and command. Default on Enterprise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept edits&lt;/strong&gt; — reads and file edits run free; good for fast iteration you'll review with &lt;code&gt;git diff&lt;/code&gt; afterward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan&lt;/strong&gt; — research and propose, but zero edits until you approve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bypass permissions&lt;/strong&gt; — nothing gated, ever. Isolated containers only, never your primary machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dimension that actually matters is reversibility. A file edit is cheap to review and cheap to undo — Claude Code snapshots files before editing specifically so you can revert with &lt;code&gt;/rewind&lt;/code&gt;. A force-push or a production migration is a different category entirely, which is why even the most permissive modes still gate that class of action separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it remembers things
&lt;/h2&gt;

&lt;p&gt;Sessions don't share memory by default — each one starts with a fresh context window. What persists is whatever you put in a &lt;code&gt;CLAUDE.md&lt;/code&gt; at your project root (read at the start of every session), plus whatever Claude saves to auto-memory as it learns your project. If you want it to reliably know "we use pnpm, not npm," write it down — don't rely on having mentioned it three sessions ago.&lt;/p&gt;




&lt;p&gt;I wrote a longer, interactive version of this — with a walkthrough diagram of the loop, the full permission-mode reference, and a practice quiz — here: &lt;a href="https://omniatlas.ai/subjects/claude-code-fundamentals?utm_source=devto&amp;amp;utm_medium=share&amp;amp;utm_campaign=launch" rel="noopener noreferrer"&gt;Claude Code Fundamentals&lt;/a&gt;&lt;/p&gt;

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