<?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: Jack Buchanan-Conroy</title>
    <description>The latest articles on DEV Community by Jack Buchanan-Conroy (@jackbcai).</description>
    <link>https://dev.to/jackbcai</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3936537%2Fff5e77c0-07fd-48e2-8c9f-94b20455f880.png</url>
      <title>DEV Community: Jack Buchanan-Conroy</title>
      <link>https://dev.to/jackbcai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jackbcai"/>
    <language>en</language>
    <item>
      <title>My Claude Code Setup for 2026: The Guardrails That Let It Work Autonomously</title>
      <dc:creator>Jack Buchanan-Conroy</dc:creator>
      <pubDate>Sun, 17 May 2026 21:35:53 +0000</pubDate>
      <link>https://dev.to/jackbcai/how-i-set-up-claude-code-to-work-autonomously-without-letting-it-wreck-the-codebase-j1a</link>
      <guid>https://dev.to/jackbcai/how-i-set-up-claude-code-to-work-autonomously-without-letting-it-wreck-the-codebase-j1a</guid>
      <description>&lt;p&gt;&lt;em&gt;The autonomy isn't in the model. It's in the repo.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most people who start using Claude Code go through the same arc. First session: it's brilliant, writes a feature in minutes, you feel like you've discovered some unfair advantage. Third session: it quietly refactors something it shouldn't have touched, formats files in a style that conflicts with your ESLint config, and confidently finishes a task that was 60% done. By week two, you're babysitting it on every step because you've been burned enough times to not trust it.&lt;/p&gt;

&lt;p&gt;That's not an AI problem. It's an infrastructure problem.&lt;/p&gt;

&lt;p&gt;I've been contracting as a principal engineer for over a decade (no CV, no job boards, just referrals) and I've watched this pattern repeat across teams. People expect autonomous AI coding to emerge naturally as models get smarter. It doesn't work that way. The model was already smart enough a year ago. What wasn't there was the engineering wrapper. The controls. The structure that makes it safe to let the agent work without you holding its hand.&lt;/p&gt;

&lt;p&gt;This is what I've learned about building that structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just give Claude the task" breaks down
&lt;/h2&gt;

&lt;p&gt;Giving Claude Code a vague task in an uncontrolled repo is like hiring a brilliant contractor who's never seen your codebase, doesn't know your conventions, has no idea what's off-limits, and reports back only when they're done. You'd never do that with a human. But we do it with AI constantly.&lt;/p&gt;

&lt;p&gt;The failure modes are predictable once you've seen them:&lt;/p&gt;

&lt;p&gt;It reformats files you didn't ask it to touch. It makes architectural decisions mid-task because you didn't specify, and it picks whatever pattern it's seen most in training data, which may not be what you use. It decides a task is "complete" when it's technically done by some interpretation, but not by yours. It runs a command that works locally but breaks in CI. It writes a secret directly into a config file because nothing stopped it.&lt;/p&gt;

&lt;p&gt;None of these are hallucinations. They're the agent making reasonable-seeming decisions in the absence of constraints. The fix isn't a better prompt. It's engineering controls that make bad decisions impossible or immediately visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  CLAUDE.md: your repo's operating manual
&lt;/h2&gt;

&lt;p&gt;The first thing to get right is CLAUDE.md. This file sits in your repo root and Claude reads it at the start of every session. Think of it as the onboarding document for a new engineer who learns fast but has no project context whatsoever.&lt;/p&gt;

&lt;p&gt;Boris Cherny, the creator of Claude Code, puts it plainly: CLAUDE.md isn't a README for humans. It's persistent memory for the agent. Everything a senior engineer would tell a new joiner on day one: how to run the build, what the test command is, what patterns the team uses, what's off-limits. That goes here.&lt;/p&gt;

&lt;p&gt;What actually goes in 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="gh"&gt;# Project Operating Rules&lt;/span&gt;

&lt;span class="gu"&gt;## Commands&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Build: npm run build
&lt;span class="p"&gt;-&lt;/span&gt; Test: npm run test:unit &amp;amp;&amp;amp; npm run test:e2e
&lt;span class="p"&gt;-&lt;/span&gt; Lint: npm run lint
&lt;span class="p"&gt;-&lt;/span&gt; Never run npm install directly — use npm ci

&lt;span class="gu"&gt;## Hard rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never modify .env files
&lt;span class="p"&gt;-&lt;/span&gt; Never commit directly to main — always branch
&lt;span class="p"&gt;-&lt;/span&gt; Don't touch files in /legacy without explicit instruction
&lt;span class="p"&gt;-&lt;/span&gt; Do not add new dependencies without flagging them first

&lt;span class="gu"&gt;## Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All API types live in src/types/api.ts
&lt;span class="p"&gt;-&lt;/span&gt; Error handling uses the centralised AppError class — no raw throws
&lt;span class="p"&gt;-&lt;/span&gt; Styling via Tailwind only — no new CSS/SCSS files

&lt;span class="gu"&gt;## When in doubt&lt;/span&gt;
Stop, describe the ambiguity, and ask. The cost of pausing is near zero.
The cost of wrong edits is high.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep it short. Every line you add eats into the context window the agent needs for actual work. The SAP community guide I've found most useful makes the point bluntly: structure your rules with the most important ones at the top, because Claude pays more attention to content near the beginning.&lt;/p&gt;

&lt;p&gt;One thing I now do after any code review: if a pattern surfaces that Claude got wrong, I add a rule. The file compounds. Over a few weeks, it becomes a genuinely useful operating constraint that stops you from seeing the same class of mistake twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate instruction files for different concerns
&lt;/h2&gt;

&lt;p&gt;CLAUDE.md is the root, but it shouldn't carry everything. For larger projects, I split concerns into dedicated files in .claude/rules/:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/
  rules/
    architecture.md    # System design decisions and why
    testing.md         # Coverage expectations, test patterns, what to mock
    security.md        # What never gets hardcoded, auth patterns, secrets handling
    style.md           # Code style beyond what linters enforce
    deployment.md      # What's safe to run vs what needs a human
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The architecture file is the one most people skip and regret. It answers the questions that no linter can: why is this service split this way, why did we choose this state management approach, what's the seam between these two modules. Without it, the agent makes coherent but wrong architectural decisions constantly, and they're the hardest to unpick later.&lt;/p&gt;

&lt;p&gt;The security file is the one that matters most for production codebases. Mine looks like this:&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;# Security Rules&lt;/span&gt;

&lt;span class="gu"&gt;## Absolute prohibitions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never hardcode API keys, tokens, or credentials — always use process.env
&lt;span class="p"&gt;-&lt;/span&gt; Never log request bodies or headers that might contain auth tokens
&lt;span class="p"&gt;-&lt;/span&gt; Never write to .env or .env.&lt;span class="err"&gt;*&lt;/span&gt; files
&lt;span class="p"&gt;-&lt;/span&gt; Never add curl | bash patterns in any script

&lt;span class="gu"&gt;## Auth&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All protected routes use the requireAuth middleware — no exceptions
&lt;span class="p"&gt;-&lt;/span&gt; JWT validation happens in src/middleware/auth.ts — don't duplicate it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These aren't suggestions. They're constraints the agent needs to treat as non-negotiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks: where "probably" becomes "always"
&lt;/h2&gt;

&lt;p&gt;Everything before this section is documentation. Hooks are enforcement.&lt;/p&gt;

&lt;p&gt;You can tell Claude in CLAUDE.md not to modify .env files. It will probably listen. If you set up a PreToolUse hook that blocks writes to .env files, it will always block them. For anyone working on production codebases, that distinction is everything.&lt;/p&gt;

&lt;p&gt;Hooks are shell commands that fire at specific points in Claude's lifecycle. Before a tool runs. After a file is written. When the agent decides to stop. They're deterministic. They don't ask the model to make a good decision, they enforce the outcome you want regardless.&lt;/p&gt;

&lt;p&gt;Here's my core settings.json setup:&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="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;"echo "&lt;/span&gt;&lt;span class="err"&gt;$CLAUDE_TOOL_INPUT&lt;/span&gt;&lt;span class="s2"&gt;" | grep -qE 'rm -rf|curl.*&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;|.*bash|DROP TABLE' &amp;amp;&amp;amp; exit 2 || exit 0"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="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;"Write|Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"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;"./scripts/block-sensitive-writes.sh"&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="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write|Edit|MultiEdit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="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;"npx prettier --write "&lt;/span&gt;&lt;span class="err"&gt;$CLAUDE_TOOL_INPUT_FILE_PATH&lt;/span&gt;&lt;span class="s2"&gt;" 2&amp;gt;/dev/null || true"&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="nl"&gt;"Stop"&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;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"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;"./scripts/verify-completion.sh"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Stop hook is underrated. By default, Claude decides it's done when it thinks it's done. A Stop hook that runs your test suite and returns exit 2 if tests are failing means Claude literally cannot declare completion until the work passes. It forces a self-verification loop.&lt;/p&gt;

&lt;p&gt;The block-sensitive-writes.sh script checks the file path against a list of protected paths:&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;FILE_PATH&lt;/span&gt;&lt;span class="o"&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;$CLAUDE_TOOL_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;'.file_path // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;BLOCKED_PATTERNS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="s2"&gt;".env.*"&lt;/span&gt; &lt;span class="s2"&gt;"*.pem"&lt;/span&gt; &lt;span class="s2"&gt;"*.key"&lt;/span&gt; &lt;span class="s2"&gt;"*secrets*"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;pattern &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BLOCKED_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nv"&gt;$pattern&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;"Blocked: attempt to write to sensitive file &lt;/span&gt;&lt;span class="nv"&gt;$FILE_PATH&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;span class="nb"&gt;exit &lt;/span&gt;2
  &lt;span class="k"&gt;fi
done

&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;One warning: if your Stop hook returns exit 2, Claude keeps working — which can cause an infinite loop. Always check the stop_hook_active field and allow stopping on subsequent invocations if it's set to true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-commit and CI as hard outer boundaries
&lt;/h2&gt;

&lt;p&gt;Hooks operate inside Claude's runtime. Pre-commit hooks and CI operate outside it entirely. That's the distinction that matters.&lt;/p&gt;

&lt;p&gt;Even with the best Claude setup, a human engineer can push without Claude. A dependency update can introduce a vulnerability. Someone runs a script that bypasses the agent completely. Pre-commit and CI are the backstop for everything, not just AI-generated changes.&lt;/p&gt;

&lt;p&gt;My .pre-commit-config.yaml for any serious project:&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;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pre-commit/pre-commit-hooks&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v4.5.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect-private-key&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check-merge-conflict&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;no-commit-to-branch&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--branch'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;main'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/Yelp/detect-secrets&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1.4.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect-secrets&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unit-tests&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;Unit tests&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run test:unit&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;always_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detect-secrets hook is the one I've seen catch things most people don't think about. Not just obvious keys, but high-entropy strings that pattern-match as credentials. Worth the setup time.&lt;/p&gt;

&lt;p&gt;In CI, the principle is simple: whatever Claude did locally has to survive the same pipeline your human engineers use. No separate lenient pipeline for AI commits. Same lint rules, same test gates, same build checks. If it fails there, it fails. No exceptions for velocity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Permission settings: what Claude can do freely vs what needs approval
&lt;/h2&gt;

&lt;p&gt;Claude Code has a permissions system that's more granular than most people use. The default is to ask about most things, which is safe but slow. The goal is to give broad permission for safe operations and lock down anything with blast radius.&lt;/p&gt;

&lt;p&gt;My settings.json permissions block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(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(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(git status)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git diff:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git log:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Grep"&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 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 push:*)"&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 install:*)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Read(./secrets/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(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 key insight from the SmartScope guide I've found most useful: permissions evaluate deny first. Setting sensitive files to deny makes them effectively invisible to Claude, which is more secure than blocking them via hooks (which still receive the attempt). For .env files and credential stores, deny is the right call.&lt;/p&gt;

&lt;p&gt;Flagging git commit and git push as ask isn't about not trusting Claude. It's about maintaining a human checkpoint at the boundary where work becomes permanent and visible to the rest of the team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design tasks so the agent works in small reversible steps
&lt;/h2&gt;

&lt;p&gt;The biggest workflow mistake I see isn't missing hooks or bad CLAUDE.md files. It's handing the agent a task that's too large to be safely reversible.&lt;/p&gt;

&lt;p&gt;"Refactor the authentication module" is not an agent task. It's a project. When something goes wrong in the middle (and something will), you have a half-refactored codebase and no clean rollback point.&lt;/p&gt;

&lt;p&gt;Break it down:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Read the current auth module and write a summary of what it does to docs/auth-notes.md. Don't touch any code."&lt;/li&gt;
&lt;li&gt;"Based on that summary, propose three approaches to the refactor. Write them to docs/auth-approaches.md. Still no code changes."&lt;/li&gt;
&lt;li&gt;"Implement approach 2, one function at a time. After each function, run the unit tests and report the result before proceeding."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step produces something inspectable. Each step is reversible. The agent can't accidentally drift into a broken intermediate state because you've defined the checkpoints.&lt;/p&gt;

&lt;p&gt;The pattern I use mentally: if a git revert on this task would feel scary, the task is too large. Aim for changes that can be cleanly undone in under thirty seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to log so you can audit what happened
&lt;/h2&gt;

&lt;p&gt;If you're running Claude in any mode with meaningful autonomy, especially overnight or unattended, you need logs. Not because you don't trust it, but because when something looks wrong later, you need to know what happened.&lt;/p&gt;

&lt;p&gt;A PostToolUse hook for basic audit logging:&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;TOOL&lt;/span&gt;&lt;span class="o"&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;$CLAUDE_TOOL_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 // "unknown"'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&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;$CLAUDE_TOOL_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;'.file_path // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +&lt;span class="s2"&gt;"%Y-%m-%dT%H:%M:%SZ"&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;ts&lt;span class="s2"&gt;":"&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;tool&lt;span class="s2"&gt;":"&lt;/span&gt;&lt;span class="nv"&gt;$TOOL&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;file&lt;span class="s2"&gt;":"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"}"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.claude/audit.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After 108 hours of autonomous operation logged and analysed (there's an excellent open-source collection of this on DEV Community by yurukusa), the most useful signals turned out to be: which files were touched, in what order, what commands ran, and how long the session ran. Not the content of changes. Git history covers that. The operational trail of decisions.&lt;/p&gt;

&lt;p&gt;I also keep a LESSONS.md in every repo I use Claude heavily on. When Claude makes a class of mistake, I document 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;## 2026-05-12: Context about rate limiter&lt;/span&gt;

Claude kept adding a new rate limiter in middleware rather than using
the existing one in src/lib/rateLimit.ts. Added explicit note to
architecture.md and a reference in CLAUDE.md.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structured incident log means Claude doesn't repeat the same category of mistake across sessions. It compounds the knowledge the same way a good post-mortem process does for a team.&lt;/p&gt;

&lt;h2&gt;
  
  
  The autonomy ladder
&lt;/h2&gt;

&lt;p&gt;The mistake is trying to jump straight to full autonomy. There's a progression that actually works, and rushing it just means more cleanup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 1: Suggestions only.&lt;/strong&gt; No writes. Claude reads, proposes, you implement. Annoying after a while, but useful for the first week in a new repo. You find out quickly whether it understands your patterns or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 2: Edits with approval gates.&lt;/strong&gt; Claude writes code, but you confirm every file change before it lands. Slow. The point isn't the speed. You're building a mental model of what the agent gets right and where it consistently drifts. Most people skip this level. They shouldn't. You end up knowing exactly which rules your CLAUDE.md is still missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 3: Test-driven tasks.&lt;/strong&gt; Write a failing test. Hand it to Claude. Tell it to make the test pass without breaking anything else. That's the entire spec. The CI gate does the verification. This is where productivity genuinely picks up, because the feedback loop is mechanical rather than conversational. Claude can't wriggle out of a red test the way it can wriggle out of an ambiguous instruction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 4: Bounded background tasks.&lt;/strong&gt; Larger scope, defined completion criteria, hook-enforced quality gates before it can commit anything. You review the final diff before it merges. Push off a task at 11pm, check the branch in the morning. But you only get clean overnight runs after you've been through Level 3 enough times to know where your constraints have holes.&lt;/p&gt;

&lt;p&gt;Most teams are ready for Level 3 within a week if they've done the CLAUDE.md and hooks work properly. Level 4 takes a month of working through the failure modes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual thesis
&lt;/h2&gt;

&lt;p&gt;AI coding agents don't become autonomous because the model gets smarter. They become autonomous when the repo becomes harder to damage.&lt;/p&gt;

&lt;p&gt;Every control described here (CLAUDE.md, the separated instruction files, the hooks, the permission settings, the pre-commit gates, the CI pipeline, the task decomposition discipline, the audit logs) is just good engineering practice. It works on every engineer in the codebase, human or AI. The reason you're adding it might be Claude, but the controls themselves aren't AI-specific.&lt;/p&gt;

&lt;p&gt;Teams aren't failing because the model can't code. They're failing because there's nothing stopping it from coding badly.&lt;/p&gt;

&lt;p&gt;When it works, really works, you push off a task at 11pm, sleep, and the branch is sitting there in the morning with passing tests and a JSONL file of everything that ran. That's the target. Not a smarter model. A tighter container.&lt;/p&gt;

&lt;p&gt;Build that, then step away.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If this was useful, I write about engineering, contracting, and building products at &lt;a class="mentioned-user" href="https://dev.to/jackbcai"&gt;@jackbcai&lt;/a&gt;. The code snippets above are production-tested — not examples invented for the article.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
    </item>
  </channel>
</rss>
