{"slug": "claude-can-t-say-done-until-the-code-is-safe-a-security-verification-loop-for", "title": "Claude Can't Say \"Done\" Until the Code Is Safe: A Security Verification Loop for Claude Code", "summary": "A developer built a security verification loop for Anthropic's Claude Code using the tool's Stop hook, which blocks the agent from declaring a task complete until its code changes pass five security pattern checks. On a test repository, the first completion attempt was blocked with five findings, prompting Claude to move an API call to a server route and switch to textContent and JSON.parse before the hook passed. The hook caps retries at three attempts per session before writing a report for human review, and the author notes the rules are pattern-based rather than a substitute for full SAST or code review.", "body_md": "I asked Claude Code to add an AI chat feature. It worked. It also:\n\nThen it said, \"All done!\" 🙃\n\nAnthropic's Claude Code team recently wrote about verification loops: Claude checks its own work and loops back to fix problems before responding. Most examples verify that tests pass. Nobody was verifying that the code was safe. So I built that.\n\nClaude Code hooks run your scripts at fixed moments. The Stop hook fires when Claude is about to finish. If the hook exits with code 2, Claude isn't allowed to stop, and whatever you print to stderr is sent back to it as feedback.\n\nThat's the whole loop:\n\nClaude says `done → hook scans the diff → exit 2 with findings → Claude fixes → hook passes → done.`\n\nStep 1: register the hook\n\n`.claude/settings.json`\n\n```\n{\n  \"hooks\": {\n    \"Stop\": [{\n      \"hooks\": [{\n        \"type\": \"command\",\n        \"command\": \"node \\\"$CLAUDE_PROJECT_DIR\\\"/.claude/hooks/security-verify.mjs\"\n      }]\n    }]\n  }\n}\n```\n\nThe hook scans only the lines Claude changed (**git diff HEAD -U0** plus new files), so old code never blocks you. Five rules, each aimed at a mistake AI code actually makes:\n\n``` js\nconst RULES = [\n  { id: 'hardcoded-secret',        test: l => /sk-(ant-|proj-)?[\\w-]{20,}|AKIA[0-9A-Z]{16}/.test(l) },\n  { id: 'secret-in-client-bundle', test: l => /(NEXT_PUBLIC_|VITE_)\\w*(KEY|SECRET)|dangerouslyAllowBrowser:\\s*true/.test(l) },\n  { id: 'llm-output-as-html',      test: l => /innerHTML|dangerouslySetInnerHTML/.test(l) && /\\b(reply|response|completion)/i.test(l) },\n  { id: 'unsafe-html-sink',        test: l => isDynamicHtml(l) }, // static strings are allowed\n  { id: 'dynamic-code-exec',       test: l => /\\beval\\s*\\(|new\\s+Function\\s*\\(/.test(l) },\n];\n```\n\n`llm-output-as-html` is the one I care most about. Suppose a prompt injection gets into your model's reply; `innerHTML = reply` hands the attacker XSS. Model output is untrusted input, always.\n\n``` js\nconst findings = scan(changedLines());\nif (findings.length === 0) process.exit(0);          // clean: Claude may finish\n\nif (attempts > MAX_ATTEMPTS) {                        // stuck: hand over to the human\n  writeFileSync('.claude/security-report.md', report);\n  process.exit(0);\n}\nconsole.error(`security-verify blocked completion:\\n${report}`);\nprocess.exit(2);                                      // Claude must keep working\n```\n\nThe attempt cap matters. A Stop hook that always exits 2 can loop forever. I count attempts per session and, after 3 tries, stop blocking and write a report for a human instead.\n\nThe hook says what is wrong. A skill in .claude/skills/security-verify/SKILL.md says how to fix it: move keys behind a server route, use textContent or DOMPurify.sanitize(), use JSON.parse instead of eval. It also tells Claude not to just rephrase code to dodge the regex.\n\nFalse positive? Claude can add // verify-ignore: on that line, and it has to tell you why. The opt-out is visible in review, not silent.\n\nOn my test repo, the first \"done\" was blocked with 5 findings. Claude moved the API call to a server route, switched to textContent and JSON.parse, the hook passed, and its final message told me to rotate the key that had already appeared in the diff.\n\nThat last part is the real win: I didn't have to remember to check.\n\nThese are pattern rules, not a full security audit. They catch the common AI mistakes cheaply on every single turn. Keep your real SAST, code review, and secret scanning in CI too.\n\n[📦 Get the drop-in `.claude` folder (hook + settings + skill, zero dependencies):](https://github.com/thesnehamk/discovering-ai/tree/main/claude-code-security-verify.)", "url": "https://wpnews.pro/news/claude-can-t-say-done-until-the-code-is-safe-a-security-verification-loop-for", "canonical_source": "https://dev.to/thesnehamk/claude-cant-say-done-until-the-code-is-safe-a-security-verification-loop-for-claude-code-pdk", "published_at": "2026-09-28 18:34:47+00:00", "updated_at": "2026-09-28 18:50:36.243949+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-safety"], "entities": ["Claude Code", "Anthropic", "DOMPurify"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-can-t-say-done-until-the-code-is-safe-a-security-verification-loop-for", "markdown": "https://wpnews.pro/news/claude-can-t-say-done-until-the-code-is-safe-a-security-verification-loop-for.md", "text": "https://wpnews.pro/news/claude-can-t-say-done-until-the-code-is-safe-a-security-verification-loop-for.txt", "jsonld": "https://wpnews.pro/news/claude-can-t-say-done-until-the-code-is-safe-a-security-verification-loop-for.jsonld"}}