Claude PreToolUse hook that blocks fable from editing files, and forces subagents to do work A developer published a Claude Code PreToolUse hook that inspects the session transcript to detect whether the active model is a "fable" model and, if so, blocks the parent session from editing files via Bash commands such as redirects, heredocs, cp/mv/rm, sed -i, and tee, while still allowing subagents to make edits. The hook builds its detection regex from named token fragments and reads the transcript file to identify the model before deciding whether to deny the tool call. | | /usr/bin/env node | | | import { readFileSync } from "node:fs"; | | | // PreToolUse: blocks file edits from the parent claude-fable- session. Subagents may edit. | | | | | | // --- regex token vocabulary jfred's regex expressions.ts style: named source-string fragments, composed with | | | // + , wrapped in new RegExp ... only at the end ------------------------------------------------------- | | | const startAnchor = "^"; // matches the start position | | | const wordBoundary = "\\b"; // a transition between a word char and a non-word char | | | const wordChar = "\\w"; // one word char: letter, digit, or underscore | | | const whitespace = "\\s"; // one whitespace char | | | const nonWhitespace = "\\S"; // one NON-whitespace char | | | const digit = "\\d"; // one digit, 0-9 | | | const oneOrMore = "+"; // one or more of the token immediately before it | | | const zeroOrMore = " "; // zero or more of the token immediately before it | | | | | | const whitespaceStar = whitespace + zeroOrMore; // \s — optional spaces | | | const whitespacePlus = whitespace + oneOrMore; // \s+ — a run of spaces | | | | | | const noneOf = chars: string : string = " ^" + chars + " "; // a negated character class ^… | | | const negativeLookahead = inner: string : string = " ? " + inner + " "; // inner must NOT follow | | | const negativeLookbehind = inner: string : string = " ?< " + inner + " "; // inner must NOT precede | | | // One of several alternative fragments ?:a\|b\|c , built with a for loop rather than .join "\|" . | | | function anyOf ...alternatives: string : string { | | | let combined = ""; | | | for let i = 0; i < alternatives.length; i++ { | | | if i 0 | | | combined += "\|"; | | | combined += alternatives i ; | | | } | | | return " ?:" + combined + " "; | | | } | | | | | | // A / redirect to a real file, excluding /dev/null and & fd dups like 2 &1 . | | | const redirectToFile = | | | noneOf "< &" + digit + negativeLookahead " {1,2}" + whitespaceStar + "/dev/null" + " {1,2}" + negativeLookahead "&" ; | | | | | | // A heredoc start: << then a word char, so it still matches once quotes around the delimiter are stripped. | | | const heredoc = "<<" + whitespaceStar + wordChar + oneOrMore; | | | | | | // cp / mv / rm / mkdir / touch / chmod as a command: at the start, or right after && / \|\| / ; / \| . | | | const commandBoundaryOperators = anyOf startAnchor, "&&", "\\\|\\\|", ";", "\\\|" ; | | | const fileMutatingCommandNames = anyOf "cp", "mv", "rm", "mkdir", "touch", "chmod" ; | | | const fileCommand = commandBoundaryOperators + whitespaceStar + fileMutatingCommandNames + wordBoundary; | | | | | | // A CLI -o