Changelog
Version history for AI Craftsman Superpowers, from v1.0.0 to v4.14.0.
Latest version: v4.14.0
Update with: claude plugin update craftsman@ai-craftsman-superpowers
v4 is a clean break
v4.0.0 requires Claude Code 2.1.218 or later and drops backward compatibility with 3.x config. The 3.9.x line stays available and frozen for older installations. See the migration guide in the plugin repository.
v4.14.0 - 2026-10-24
Two guards, a faster gate, and a cockpit you see without opening it. A git command that switches the project’s hooks off is refused (CR-258), and a red test waved away as flaky without a ticket sends the turn back (CR-289). SEC001 covers JavaScript and Python with today’s key shapes (CR-257), files a Bash command changed are reviewed (CR-256), and a test run that tested nothing grants no evidence (CR-255). A write costs about half of what it did, a prompt a third, and a session start half (CR-278, CR-290). On Claude Code the cockpit now ships in the plugin: a line above the prompt says where the session stands, and each answer that changed it gets a receipt.
Upgrading can refuse a file that 4.13.1 let through: a credential literal in
a .js, .jsx, .mjs, .cjs or .py file, and a key with a recent issuer
prefix (sk-ant-, sk-proj-, glpat-, npm_) in any covered language.
SEC001 is never ignorable: move the value to the environment and rotate it.
The cockpit now ships in the craftsman plugin. If you installed the separate
craftsman-cockpit plugin, uninstall it: its 4.14.0 is empty, so updating it
also removes the old module.
Added
- On Claude Code, a line above the prompt says where the session stands: the
writes the gate judged, the blocks still open with their rule and file, the
warnings, and what the tests say (red with the run that failed, verified, a
run that tested nothing, not run). It appears once the session has
something to report and is redrawn after every write the hooks judge;
Ctrl+X then Ctrl+A folds it. Each answer that changed it gets one line
under it, as in
craftsman: 3 writes, 1 blocked, 0 warnings · 1 open: LAYER001 Order.php · tests not run; a turn that only read or talked gets none. Both readcraftsman-helper session status, a JSON view of what the hooks keep for the session, and decide nothing (ADR-0031). Measured in a headless Claude Code 2.1.310 session on a synthetic project: the receipt line came from the real hooks through the real helper. - SEC001 covers JavaScript and Python, and knows today’s key shapes. An
Anthropic or OpenAI project key was written to a
.tsfile with no finding while the repository’s own secrets scanner knew both prefixes, and a.jsor.pyfile got no SEC001 at all. The shapes now live in one list,hooks/lib/secret-patterns.sh, that every pack reads: Anthropicsk-ant-, OpenAIsk-proj-,sk-svcacct-,sk-admin-and legacysk-, GitLabglpat-, npmnpm_, Slack webhooks, GitHubghs_,ghu_,ghr_, Striperk_live, AWSASIA, a typedAPI_TOKEN: str = "...", and a*_KEYconstant holding a 32-character key. Issuer prefixes match with case and never inside a longer word, soAsia/Kolkata,task-...ids and storage keys stay silent. An env read on the line no longer hides an issued key:process.env.KEY ?? "sk-ant-..."ships the key. This reverses the CR-231 choice to leave secrets out of JavaScript (CR-257). - A turn no longer ends on a red test waved away. A new synchronous Stop
hook,
test-dismissal-guard.sh, sends the turn back when a test run that failed in the session is still outstanding and the last message calls it flaky, intermittent, known, pre-existing, unrelated, or green alone, with no ticket named (ORD-42,#123, an issue URL). A test red in the suite and green alone depends on shared state, order, time or the filesystem: a defect, not noise. Blocks in strict, warns in moderate, silent in relaxed; a second stop on the same excuse is shown to the user, never looped. The words are data, aTEST_DISMISSALcategory of the bias patterns (English and French)./craftsman:debugand/craftsman:verifyname the excuse among their red flags, with pre-existing deprecations reported instead of fixed (CR-289). - The agent can no longer switch the project’s git hooks off. A new
PreToolUse hook,
git-bypass-guard.sh, refuses--no-verifyon any git subcommand,git commit -n,-c core.hooksPath=...,git config core.hooksPath <dir>,HUSKY=0,SKIP=...,LEFTHOOK=0, and agit stashrun in the same command as a commit, with the reason. A model has been seen committing six times in a row with--no-verifyandgit stashagainst rules forbidding it (anthropics/claude-code #40117): a rule the model reads is advice, a refusal is not. The command is read token by token, so--no-verifyquoted in a commit message passes. It runs on the git subcommands Claude Code’siffilter sees, which is best effort (env git ...andsh -care not seen), and fails closed. The user keeps the choice: run the command themselves, or sethooks.disabled: git-bypass-guard(CR-258). - Files a Bash command changed are reviewed. Claude Code 2.1.269+ lists them
in
tool_response.bashEditDiff;post-write-check.shnow runs after every Bash call and judges each listed file as a Write, under its own path. A call that changed nothing leaves before any library loads. The host records the list in bypass and auto mode, elsewhere only withbashEditDiffEnabledin user or managed settings, and documents it as best effort: the pre-write gate remains the only refusal before the disk (CR-256).
Changed
- The latency suite’s ceilings follow the hooks down: post-write from 6.5 to 3.55 times the calibration baseline on macOS and from 11.4 to 4.9 on Linux, the other rows alike, from four CI runs on the final code and a maintainer’s laptop under load. Left where they were, they sat at twice what the hooks now cost, so a doubling would have passed.
- The cockpit moved into the craftsman plugin: one install, and
/instinctsfinds the core at the plugin’s own root instead of through the session binding, the repository layout andPATH, the lookup two field failures came from (CR-279, CR-291). Codex and Grok never see the module: it is declared byhooks/claude-code.json, which only Claude Code’s manifest names, whilehooks/hooks.jsonholds none. Measured: a module that fails to load leaves the command hooks running on 2.1.310, and Claude Code 2.1.218, the minimum, ignores the module file and runs the hooks (ADR-0031 amended). - Every write costs less: a post-write about half, a pre-write about a
third less (median of seven alternating runs against the previous code,
2043 to 1030 ms and 1421 to 905 ms on a loaded machine). Claude Code had
started offering to disable the plugin’s slow hooks. Each hook now reads
a pack manifest once instead of four times through subshells, a write
reads the metrics schema stamp instead of running ten migration probes,
and the craft config is read in one jq instead of five per custom rule
(CR-278). A second pass, from a profile rather than a guess, took a
further 16 percent off a post-write and 15 off a pre-write (CPU time,
alternating runs against the previous code: 600 to 503 ms and 558 to
480 ms). The manifest memo of the first pass searched every pair for
each question, 2,200 loop turns per hook; it now jumps to the key. The
PHP root namespace lookup ran a process per directory up to
/, the project root behind every metrics row rangitfour times, and each pack manifest had its ownstat; each is now done once or in-shell. - A prompt waits less before the model sees it: the bias detector, which runs on every prompt, takes about 70 percent less CPU (alternating runs against the previous code under the same load: 478 to 144 ms on a prompt with no bias, 496 to 149 ms on a signal hit), and the latency suite reads it at 0.50x its baseline instead of 1.60x. The pattern registry derived each language’s variable suffix in a subshell, thirteen times for every pattern it was asked for: 221 subshells per prompt. It now derives them once, when it loads. The reported-speech check ran once per category instead of once per prompt, and the pending Sentry request was read by a Python start where jq does (CR-278).
- A session starts in about half the time: SessionStart, which Claude Code waits for before its first response, went from 1630 to 900 ms at startup and from 1680 to 950 ms after a compaction (alternating runs against the previous code, on a copy of a 26,000-violation metrics database), same banner. On macOS each pack skill cost a python3 start to compute a relative path; the known-language registry was rebuilt by each of eight subshells; the healthcheck and the routing table ran an awk and a grep per skill; and the metrics database ran its fourteen migration probes although it carried this version’s schema stamp. Each is now done once or in-shell. The cost: SessionStart no longer re-runs the migrations over a database damaged by hand whose stamp still matches, the same trade the write hooks already made (CR-290).
- SessionStart sweeps the files of sessions that ended without a SessionEnd once a day instead of on every start: what it takes is a week old, and it cost each start a find and a Python start (32 ms). The sweep had no test; it has three now.
- The structural ratchet records function-level debt only: worst-function
complexity, longest function and suppression count. A file that grew with a
feature failed the ratchet until someone raised its budget: on this
repository 270 of 473 raises moved
file_linesalone, while LOC001, the rule that owns file length, is advisory.file_linesandfan_outare still printed byratchet.py measureand dropped from an older row on its next update. A raise keeps one reason, the newest (git keeps the history), andratchet.py check --tightrefuses a mark looser than its file or a touched file with no row, so a pipeline can require the tightened mark to be committed. ADR-0025 amended (CR-288).
Fixed
-
A compaction no longer restarts the session’s metrics. SessionStart fires again after a compaction, with the same session id, and it rewrote the session’s start time and deleted its write and violation tallies: a session that ran five days and compacted five times was measured from its last compaction, with every earlier write gone. After a compaction the session now keeps both; a session that starts, resumes, forks or clears restarts them as before (CR-296).
-
craftsman-ciscans a file once however it is spelled on macOS. The key that deduplicates the scan folds//, and under bash 3.2 that fold kept a backslash, sosrc//Order.phpandsrc/Order.phpwere two files and their findings counted twice. Linux runners, on bash 5, were not affected (CR-297). -
Pack skills no longer disappear while a session starts. SessionStart deleted every pack skill and copied it back, and Claude Code’s hooks reference says skill discovery runs before SessionStart hooks finish, so a session that looked in between could start without them (deduced from that timing, never observed). A skill a loaded pack still serves is now refreshed in place (CR-290).
-
The pack manifest identity list from the second CR-278 pass never answered under bash 3.2, the macOS shell:
${path//\/\//\/}keeps the backslash there, so no lookup matched and every manifest was stat’ed on its own. Answers were right, the saving was not there. A test now fails when a manifest the list holds is stat’ed (CR-290). -
A new interactive Claude Code session was taken for a Codex session: its SessionStart carries
modeland noprompt_id, which the host detection read as Codex. The session binding went under~/.codex, the startup healthcheck talked about Codex, and the cockpit mod’s/instinctspane said “craftsman-helper not found”. Found on 32 sessions of one machine since 4.12.3. Claude Code’sscratchpad_dirnow names it, andmodelmeans Codex on SessionStart only with Codex’spermission_mode; the interactive payload is captured as a fixture (CR-291). -
A test run that failed no longer has its failure erased by a narrower run that passes. The suite went red on one test, the agent ran that test alone, it passed, and
post-bash-test-verify.shgranted the verification evidence and cleared the failure: the plugin endorsed “flaky, passes alone” instead of refusing it. Each red run now stays outstanding until a run that adds no argument to it passes (hooks/lib/test_scope.py): a filter, a path or a test name may narrow the run, and which flags narrow is each runner’s business, so any added argument counts. Another suite going green does not answer this one either. The cost of that doubt is one re-run of the red command (CR-289). -
The structural ratchet measured every PSR-12 method (opening brace on its own line) as complexity 0 over one line, and a multi-line signature as its signature only: it closed the function on the brace. It now finds the body after the header, so PHP methods and functions with long parameter lists are measured. The ruler changed (instrument 3): a mark taken by the previous one is re-taken on the next touch, without failing an untouched file (CR-288).
-
Re-taking a mark after an instrument change erased the row’s reason and the rule baseline’s counts stored with it, so inherited rule findings would have blocked again after an upgrade. Only the measured numbers are re-taken now (CR-288).
-
A test command that exits 0 no longer grants verification evidence when it tested nothing: an npm script that only echoes, a suite with every test skipped, PHPUnit with zero assertions. Packs declare the wording in a new
empty_run_markerscapability, each marker taken from a real capture intests/fixtures/test-runs; the skill’sset-verifiedis refused until a run that tested something passes (CR-255). -
A plugin update inside a session left the session binding on the old plugin root, so the cockpit mod ran the previous version’s helper (
/instinctsprinted a usage line in its pane). The binding now follows the root the hooks run from, and the pane names the helper it ran when it fails (CR-279).
Removed
config/default-config.yml. It said it was copied to~/.claude/.craft-config.ymlon first run; no code has read it since the v4 configuration format, and it described that file in the pre-v4 one (version: "2.1", apacks:mapping). The configuration followsschemas/craft-config.schema.jsonand/craftsman:setup.
v4.13.1 - 2026-10-20
A refusal no longer offers a craftsman-ignore marker that the rule ignores
(CR-230). LAYER004 recognises the Domain under any root namespace your
composer.json declares, not only App\ (CR-244). The FAQ and both READMEs
now say which PHP rules hold on any framework (CR-242).
Upgrading can refuse a file that 4.13.0 let through: raw SQL in a Domain
namespace under a root other than App\, outside a /Domain/ path. To phase
it in, set LAYER004: warn for that directory in its .craft-rules.yml.
Fixed
- LAYER004 (raw SQL or DQL inside the Domain) recognised a Domain namespace
only as
App\Domain, so a project whosecomposer.jsondeclares another root (Acme\, a Slim or Laravel application, a monorepo package) passed it outside a/Domain/path. It now reads the root from the psr-4 map, as LAYER001 to LAYER003 already did, through one shared reader in the PHP pack (CR-244). - The FAQ said language rules “only activate for the packs you enable”. Every
shipped pack judges its own files whatever the
stacksetting says, and the setup’s framework question and the pack’s Symfony name read as “Symfony only” to a PHP developer on Slim. The FAQ and both READMEs now say which PHP rules hold on any framework, which two rule behaviours know Doctrine, and thatstackswitches no shipped pack’s rule;tests/core/test-doc-claims.shreads the pack manifests and the namespace reader before it reads the documents (CR-242). - A refusal offered a way out the gate then refused: SEC001 ended on “Use //
craftsman-ignore: <RULE_ID> to suppress”, a marker SEC001 to SEC003 never
honour (
never_ignorable: true), the post-write JSON and the project-wide pattern said the same, and post-write’s stderr claimed no SEC rule takes a marker, false for SEC004.rule_marker_advicenow reads the manifest: the marker is offered only to the rules that honour it, the others are named as “No marker silences …: fix the code.”, in stderr, in the JSON, in the warning actions, and in the exported doctrine (no marker). The subagent gate let a marker hide SEC001; it honours the manifest like the write gates (CR-230).
v4.13.0 - 2026-10-19
JavaScript gets the craftsman rules (.js, .jsx, .mjs, .cjs, native
JavaScript and jQuery included), SEC004 refuses unescaped values reaching an
HTML sink in TypeScript and JavaScript, the instinct review pane ships as the
craftsman-cockpit companion mod (ADR-0031), and the hooks follow Claude
Code’s recent changes: background test runs, the Task tools, a write gate that
fails (CR-218 to CR-220, CR-226, CR-231).
Upgrading can refuse a JavaScript file that 4.12.3 wrote without a word. To
phase the rules in on an existing codebase, set them to warn for a directory
in its .craft-rules.yml.
Fixed
-
A test suite run in the background never counted as verification evidence: Claude Code 2.1.277 removed
TaskOutput, the only event that carried its result. On 2.1.302 the run ends as a task notification andUserPromptSubmitfires on it, sopost-bash-test-verify.shnow also listens there (async) and reads<task-id>,<status>and the exit code, tied to the Bash that started it by task id. Captured, not documented for shell tasks: seetests/fixtures/hosts/PROVENANCE.md(CR-218). -
/craftsman:team,/craftsman:planand theteam-leadagent required TaskCreate/TaskUpdate/TaskList, which Claude Code no longer offers on Opus 4.8, Sonnet 5, Fable 5 and newer withoutCLAUDE_CODE_ENABLE_TODO_TOOLS=1. Each now says what to do without the Task tools: coordinate through messages and keep the ledger in the reports, as the agent teams documentation prescribes (CR-219). -
A write gate that timed out, was killed or could not start let the write through.
pre-write-check.shandconfig-protection.shnow declareonFailure: "block"(Claude Code 2.1.295); older builds ignore the key (run on 2.1.294).pre-push-verify.shstays advisory (CR-220). -
The PHP001 auto-fix wrote a file PHP refuses to compile when code shared the opening line:
<?php echo 1;came back withdeclare(strict_types=1);below it (“strict_types declaration must be the very first statement”). The declare now goes right after the tag and that code moves below it; a first line that is not a real opening tag (<?phpx) is refused instead of rewritten.php -ljudges the result (CR-226). -
On Grok,
instincts approverefused.grok/skills, the one directory Grok reads skills from:instinct_skills.pykept its own list of host directories with Claude Code’s and Codex’s only. The destinations now come fromhooks/host-capabilities.json, which declaresuser_skills_dirfor Claude Code and Codex; global promotion on a host that declares none (Grok) is refused instead of guessed. -
The suite’s shared assertions could give the wrong verdict on a long text:
echo "$text" | grep -qunderpipefailreports the writer’s SIGPIPE when grep stops at the first match, soassert_containsfailed on a text that held the needle (macOS CI) andassert_not_containspassed on one that held the forbidden text. Measured on a 1 MB text: 100 wrong verdicts out of 100, none with a here-string, which they read now (tests/core/test-assert-helpers.sh).
Added
-
JavaScript gets the craftsman rules:
.js,.jsx,.mjsand.cjs, native JavaScript and jQuery included. Asrc/domain/order.jsimporting../infrastructure/dbwas written with no rule at all while the same file as.tswas refused. Thejavascriptlanguage of the react pack now runs LAYER001, LAYER004, SEC002 and SEC003 through the same function bodies as TypeScript, so the two cannot disagree, plus ESLint at Level 2, the protected linter configs and the test runners (mochaadded). The TypeScript-only rules (TS001 to TS003) and SEC001 (secrets, repository security rather than code quality) stay out of it (CR-231). -
SEC004, TypeScript and JavaScript: an HTML sink fed by a value the code did not write as a literal (
innerHTML,outerHTML,document.write,insertAdjacentHTML, jQuery.html(),dangerouslySetInnerHTML, and interpolated or concatenated markup passed to.append()and its siblings).DOMPurify.sanitize(...)is accepted. Unlike SEC001 to SEC003 it is ignorable with a statedcraftsman-ignore: SEC004, because “a variable reaches a sink” is a heuristic. -
ADR-0031: function-hook mods are a companion surface for Claude Code, never a gate. A mod lives in
mods/<name>/as its own plugin, reads and writes throughbin/craftsman-helperonly, and registers no tool that could reach a decision reserved for a person. -
craftsman-cockpitmod:/instinctsopens a pane listing the project’s candidate instincts with their evidence, in Wilson-bound order, with Approve and Reject buttons wired toinstincts approve|reject(#45). Install:/plugin install craftsman-cockpit@ai-craftsman-superpowers. -
The instinct pane explains each candidate: the rule’s wording, pack and default severity, the acceptance rate with refusals split between
craftsman-ignoreand config, the last fix date, what Approve and Reject do, and a preview of the exact skill Approve writes. -
A learned skill whose corrections recorded no context carries the rule’s own wording as its pattern instead of “see the rule definition”.
-
craftsman-helper instincts review(hooks/lib/instincts_review.py): the review queue as JSON (candidates with evidence, approved instincts), refreshed likecandidates; repository text passes the same single-line filter a generated skill uses. -
tests/mods/test-mods.sh: the ADR-0031 rules on every mod, each proved able to fail on a fixture, plusclaude plugin validateandclaude plugin testwhen aclaudebinary is present.
Changed
-
LAYER001 and LAYER004 also read
require(...)andimport(...), not only ESMfrom. A TypeScript domain file that reached infrastructure throughrequirewas let through before and is now refused. -
/craftsman:metricsstep 9 readsinstincts reviewand presents each candidate from its fields (wording, severity, refusal split, last fix, evidence, the exact skill Approve writes), the same data as the cockpit pane, on every host. Repository text in the queue is quoted as data. -
instincts approve,reviewandpromotetake the skills directory as an optional argument: left out, it is the current host’s, so the skill no longer spells a directory per host. -
scripts/native-manifests.pykeeps only the root plugin in the Grok catalogue: no other host loads a mod. -
A mod’s version lives in its marketplace entry only, which
scripts/bump-version.shalready carries.
v4.12.3 - 2026-09-28
Bugfix release: the PHP analysers report a run that gave no verdict, PHPStan never runs without its pinned configuration, and the CI comment heading agrees with the job’s exit (CR-211, CR-212). No rule changes.
Security
- PHPStan no longer runs when its pinned configuration cannot be written.
The fallback ran it bare, and a bare PHPStan auto-discovers the
repository’s
phpstan.neon, whosebootstrapFilesrun PHP (CR-212).
Fixed
- PHPStan and deptrac report a run that gave no verdict as ANALYSER001, like the ESLint, dependency-cruiser, errcheck and clippy adapters. A fatal PHPStan error, a deptrac run that failed before analysing (a missing depfile) and a run the budget stopped all left the gate as silent as a clean file. deptrac’s exit 1 counts as a verdict only when it printed a finding line. A clean deptrac run still declares no coverage of LAYER001 to LAYER004: exit 0 with nothing printed is also what a depfile that does not cover the boundary produces, and the regex keeps reporting then (CR-212).
- The CI comment heading agrees with the job’s exit. A report that scanned
no file outside
--changed-onlyread “Passed” while the job failed with exit 2; the heading now comes fromadapter_compute_exit, and a zero-file comment says whether the diff held nothing to validate or the run did not happen (CR-211).
v4.12.2 - 2026-09-28
Bugfix release: the P2 findings M1 to M18 and I1 of the review of main eb54d13 (CR-171 to CR-178), and macOS latency ceilings set from measured spread (CR-179). One new advisory rule, ANALYSER001; no rule becomes stricter.
Upgrading
- Configuration:
/craftsman:setupnow writes a v4 file (v: 4, a scalarstack,strictness,guided). A pre-v4 file (version: "1.0"with astack:mapping) was silently read asfullstack; the healthcheck now names it as a warning with the keys the resolver ignores. Rerun/craftsman:setupto migrate. A v4 file with an invalid value is an error. - Custom rules declared in
.craft-config.ymlnow run before the write as well as after it. Aseverity: blockrule refuses the write instead of refusing the file once it has landed. An edit to a file that already violates such a rule is refused before the write, as pack rules already are; the rule baseline mark still demotes inherited occurrences. The hook message gains aline N:prefix. - Rust with clippy trusted: a clean clippy run now supersedes RUST001
and RUST005 as
packs/rust/pack.ymldeclares, so an unwrap is reported as the advisory CLIPPY001 instead of the blocking RUST001.
Security
- The secrets scan reads the repository it is given.
.github/scripts/secrets-scan.shlisted tracked files relative to the target but let grep open them relative to the caller’s working directory, so a key in the target could come back SUCCESS. The target is now canonicalised and entered before any read (CR-177). - Exported host gates run whatever the plugin root is called. The
grok-hooksexport spliced the raw root into the command body, so a root containing$,"or a backtick was expanded or split and the gate exited 127 without running. Each manifest command is rebuilt word by word with the root quoted; a manifest command that is more than plain words is refused at export instead of being written with another meaning (CR-177).
Added
- ANALYSER001 (advisory,
rules/core.yml): an analyser that gave no verdict is reported. A crashed or stopped ESLint, dependency-cruiser, errcheck or clippy left the gate as silent as a clean file. The message names the tool and its exit status, never its stderr (CR-174). dashboard.py <db> --score [DAYS](throughcraftsman-helper dashboard --score 7): the quality score over the last DAYS days, the DAYS before, and the trend.
Fixed
- Custom rules: one pass,
rules_check_custom, shared by pre-write (on the would-be file), post-write andci/craftsman-ci.sh. Each rule is reported once, at its first matching line, with severity resolved per file.tests/adapters/test-parity.shchecks the three front-ends at block, warn and ignore (CR-171). - Rust: an external
#[cfg(test)] mod tests;opened a test range that ran into the body of the next production function, whose.unwrap()lost RUST001 in the scanner, the hooks and the pipeline. The range ends on the item’s own;or closing}(CR-172). - Analyser coverage reaches the flush. errcheck and clippy declared the rules
they answer for from inside a command substitution, and the declaration
died with the subshell: an errcheck finding came out twice, a clean
errcheck left GO006 standing, a clean clippy left RUST001 in place.
Adapters now return an
@covered <RULE>record that the hook and the pipeline apply before the flush (CR-172). - errcheck runs under
sa_timeoutwith the per-file budget. Only its two verdicts (exit 0 and 1) answer for GO004 and GO006; a fatal exit or a stopped run leaves them to the regex (CR-172). - Semantic fix witness: the content hash that proves a file changed between
two verdicts was looked up by directory, so a run on
src/Domain/B.phpbecame the witness for an unchangedsrc/Domain/A.php, and a CLEAN on A recorded a fix.haiku_runsrecords each run’s exact path (additivefile_pathcolumn), and a finding with no recorded content for its own file stays open (CR-173). - Quality score: one formula,
quality_scoreinhooks/lib/dashboard.py(findings per write: blocked 1, warned 0.3, eachfixedcorrection gives back 0.6)./craftsman:metricscomputed its own score fromaction='fix', a value the corrections table never holds, so the fix bonus was always zero (CR-173). - SubagentStop gate: rule severities are honoured. It never initialised the
rules engine and
add_warningbypassedrules_severity_for_file, so a rule set toignorestill reached the main loop and ablockrule was filed as a warning (CR-174). - Final review scope includes untracked files that
.gitignoredoes not exclude. A class created during the session and never staged was absent from the review that looks for new classes without tests (CR-174). /craftsman:setupStep D takes the canonical baseline withcraftsman-ci baseline, which records rule violations as well as structure, so inherited debt reports as warnings instead of blocking the first edit (CR-175).config_validateinhooks/lib/config.shchecks a config against the schema with the resolver’s own reader; the healthcheck config row uses it (CR-175).- doc-writer and ui-ux-director no longer open on a shell command their tools allowlist refuses; they start from the context their caller provides. The turn-budget suite fails when an agent body prescribes a shell command without Bash in its tools (CR-176).
- security-pentester no longer isolates into a worktree, where the uncommitted changes it audits are absent. The turn-budget suite refuses worktree isolation on any agent that reviews or audits (CR-176).
- The Mikado method, the refactor and legacy skills, the legacy-surgeon agent
and the characterization example no longer prescribe
git reset --hardor a path checkout to undo an attempt. A mark taken before the attempt reverses exactly what the attempt changed, and the user’s uncommitted and untracked work survives.tests/core/test-attempt-revert.shruns every documented sequence on a throwaway repository (CR-176). The skills exported to Hermes carry the same correction: the committed export was regenerated without a drift check, so it still prescribedgit reset --hard, andtests/adapters/test-hermes-plugin.shnow fails when regenerating it changes a file. - An unreadable CI report is never commented as Passed. The shared comment
formatter fell back to zero counters while the job failed with exit 2; it
now renders
Craftsman Quality Gate -- Error, checked for github, gitlab, bitbucket, jenkins and generic (CR-177). - The PHPStan diagnostic example checks the
trust_project_toolsconsent and calls Level 2 active only after a probe shows a finding (CR-178).
Changed
- ADR-0023 gains an amendment recording what shipped: the test-failure wake-up, the evidence gate and its exemptions, a single monitor, and a pre-push hook that only warns (Hermes is the one host that refuses a push). README (both mirrors), SECURITY.md, references and guides are aligned (CR-178).
- macOS latency ceilings are set from 30 measured runs of the runner’s own
spread instead of a guess, and
tests/perf/test-hook-latency.shfails when a ceiling exceeds 1.5 times its quiet reading, so a ceiling cannot drift into meaninglessness (CR-179).
v4.12.1 - 2026-09-27
Bugfix release: the Hermes gate findings B6 to B8 of the review of main eb54d13 (CR-170) and their Strix follow-up, a Python pack false positive, and a repository layout fix. No configuration or rule changes.
Security
- Hermes terminal gate: the command is read the way a shell runs it.
echo ok;git push,env git push,sh -c "git push",eval git push,$(git push)and a git alias for push all went through whilegit pushwas refused. Operators count even glued to a word, wrappers and assignments are stripped, nested shell text is read, aliases are resolved, and a push or commit the reader cannot place is refused asunknown. - Hermes terminal gate: a push is judged on what it publishes. After a pass
on HEAD,
git push origin unsafepublished another branch’s tree. Every refspec source must carry the judged tree;--all,--mirror,--tags, glob refspecs,push.default=matchingandremote.<name>.pushrules are refused because their sources cannot be listed. Deleting a remote branch still passes. - Hermes terminal gate: every gated git call of a compound command is judged
(Strix review). Only the first was, so
git push origin HEAD; git push origin unsafepublished a tree nobody judged. Several pushes are each judged; a commit next to a push in one command is refused, since the commit the push would publish does not exist yet; a push naming a ref that does not exist says so. - Hermes conclusion gate: a CI report is a verdict only when it parses, carries its summary, agrees with its exit status and covers the changed source files. An unparsable report or a zero-file report with exit 2 was read as no finding and recorded a pass; it is now an unknown verdict, and the conclusion waits. A turn that changed no file of a known language still passes. A language registry that cannot be read is an unknown verdict too, never a turn with nothing to judge.
Fixed
- The Python pack’s WARN-PY001 counted the commas of a return annotation as
parameters:
def resolve(verb, args, where) -> tuple[str, list[str]]was reported as a function with four or more parameters. The pattern now stops at the closing parenthesis.
Removed
.codex/config.tomlis no longer in the repository. It was a contributor’s own Codex project layer (it set a Claude Code flag in Codex shells) committed by mistake, next to.codex-plugin/, the manifest Codex actually installs..codex/is ignored like.grok/.
v4.12.0 - 2026-09-27
Native install on the three hosts that load plugins (Claude Code, Codex,
Grok), a Grok write gate that actually runs, and per-host session isolation.
Hermes keeps its own plugin manifest (plugin.yaml).
Upgrading
- Grok: run
bin/craftsman-grok-installagain (orcraftsman-ci export --target grok-hooks --into ~/.grok/hooks). The gate that runs is now the global~/.grok/hooks/craftsman.json; a project.grok/hooksfile from 4.11.0 still needsgrok --trustto run. - Codex: install from the native entry point
(
codex plugin marketplace add BULDEE/ai-craftsman-superpowers, thencodex plugin add craftsman@ai-craftsman-superpowers) and trust the handlers in/hooks. Installing never grants trust. - Skills and scripts that read the metrics database from a shell: resolve
it with
bash "$(craftsman-path bin/craftsman-runtime)" metrics. The~/.claude/craftsman-metrics-db-pathbridge is a Claude Code fallback only; native Codex and Grok sessions never read it. /craftsman:healthchecknames the export command when a gate is missing, behind this install, or written by another install.
Added
- Grok native install (
.grok-plugin/plugin.json,.grok-plugin/marketplace.json) andbin/craftsman-grok-install, which installs the plugin and writes the global gate. Grok lists a plugin’shooks/hooks.jsonashookType: fileand runs none of it on a fresh process (measured on 1.0.34 and 1.0.40), so the gate is exported from the same manifest instead of typed twice. It judgeswriteandsearch_replacebefore disk and keepspre-push-verify.sh. When no other review CLI is installed, the semantic review falls back togrok -p. - Codex native entry point (
.codex-plugin/plugin.json, catalogue in.agents/plugins/marketplace.json). The native loader reads the 22 skills and 14 handlers without the Claude manifest. Codex 0.155.1 omits hooks from the portable rootplugin.jsonformat, so the package ships no competing root manifest. - All 12 agent missions ship as ordinary files before the first session.
scripts/native-manifests.pygenerates the Codex and Grok manifests and--checkfails on drift between the six pack agent copies and their sources;scripts/bump-version.shruns it. bin/craftsman-runtime: one resolver for the current host and session store (metrics), used by skills and one-off queries.- Grok slash names:
/name, except/plan,/loopand/workflow, which Grok already owns and which stay/craftsman:name. - The shared
hooks/hooks.jsonmatchers also name Grok’s write tools, and the healthcheck gained awrite-gaterow: ok when the gate’s owner marker matches this install, the export command otherwise.
Changed
- Every session store is bound to its host and session. A validated binding from the hook payload wins over inherited environment variables, Grok bindings are recovered from all supported aliases after a hook reload, and an unbound native session reports missing state instead of reading another host’s data. Metrics and correction persistence never bypass validation when the store is unavailable: the write gate still judges the code.
- The session context is resolved in one Python process instead of several helper processes per hook.
- Host facts live in
hooks/host-capabilities.jsononly. The gate exporter (ci/host_hooks.py) reads each host’sgate_fileandgate_exportrow (timeout, native environment names, tool aliases, conditional handlers, data directory) and derives the events a host does not fire from itsevents_loaded. A test fails when a host name, host environment name or host tool name is typed in the exporter again. craftsman-ci export --target codex-hooksis refused and writes nothing. Codex runs the plugin’shooks/hooks.jsonnatively once trusted and reads user or project hooks only fromhooks.jsonorconfig.tomlnext to a config layer (Codex hooks), never from.codex/hooks/*.json: the file this target wrote was read by nothing.bin/craftsman-grok-installreadsgrok plugin list --jsonto know whether this checkout is already installed, instead of matching the wording of an install error, and prints the checkout’s origin and commit before trusting it.
Fixed
-
Grok’s default hook timeout is 5 seconds and fail-open. The Grok export alone sets
timeout: 15on every handler.hooks/hooks.jsonstays without a timeout, so Claude Code and Codex keep their 600 second default. The export sets each host’s own root and data variables (GROK_PLUGIN_*orPLUGIN_*) and still setsCLAUDE_PLUGIN_*, which the engine scripts read on every host. A Codex gate does not carryGROK_*. The gate file is replaced atomically (mkstempin the same directory, thenos.replace), keeping the existing mode. -
Each generated gate names the install that wrote it. The healthcheck is ok when the marker matches, and names
craftsman-ci export --target <host>-hookswhen the same install is behind. A 4.11.0 gate with no marker is a warning, and SessionStart adopts it, when itsCLAUDE_PLUGIN_ROOTis this install. Any other root is an error that names both paths and the export command, and the file is left untouched. The export, the healthcheck and the refresh share one relative path,.<host>/hooks/craftsman.json.craftsman-grok-installstill exports the gate when the plugin is already installed, and prints the installer’s own output on success. -
Session lifecycle stores are isolated per native session: start, write, verify, failure, compact and end read and write the same store, and a parent session’s sentinel is left intact (#103).
-
Pre-write caches are scoped to the bound session. A cache filled for one store could answer for another after the store changed between two
pack_loader_initcalls. -
Grok config protection covers the host’s own hook configuration, so the model cannot relax the gate through an edit.
-
Hermes exported skills expand the native helper paths.
-
Hook libraries import their siblings under the Python 3.9 floor.
-
The Linux latency ceilings in
tests/perf/test-hook-latency.shwere inside the spread of one commit onubuntu-latest. They are now 1.35x the quiet reading and at least 1.15x the fast one, still under twice the quiet reading, so a 50 percent regression still fails. -
/craftsman:healthcheckno longer warns about the write gate on every Claude Code session. The row warns only where the host’s capability row records a trust step (Codex: review the handlers in/hooks), and is ok where plugin hooks run on install.
Security
- Test evidence is granted from shell grammar, not from text separators.
hooks/lib/command_grammar.pylexes the command withshlex, so a quoted or escaped runner (printf '%s\n' 'hello; ./run-tests.sh --quick') no longer setsverified, which gates a push. Runners after&&, by path, and throughpython -mornpxstay qualified. - Redirections are part of the command, not separators. Before this release,
pytest -q 2>&1lexed as2>,&,1, so a failing run kept the evidence green;&>,>/dev/null 2>&1had the same hole, and|&read as one word hid a pipe.<and>are now lexer punctuation,|&is a pipe, and a command sent to the background (pytest -q &) grants and revokes nothing, since its status is the shell’s. - A runner fed by a pipe (
echo n | ./run-tests.sh) no longer grants the evidence: it ran on input the model chose. A failure there still revokes. - The Grok semantic review runs with the read-only allowlist of Grok’s
headless guide (
--tools read_file,grep,list_dir). The previous denylist named the shell toolrun_terminal_cmdwhile hook payloads name itrun_terminal_command; an allowlist with a wrong name fails closed. - Config protection judges the file a write REACHES, whatever its spelling.
A bare relative path,
//,/./, an inner.., a symlinked parent directory and a case change (.Claude/Settings.json,PHPStan.neon, an upper-cased plugin path) all reached a protected file with exit 0 on Claude Code and Grok. Each path is now joined to the payload’scwd, resolved physically, and classified case-insensitively. - Under
bypassPermissionsthe gate’s own configuration (.craft-config.yml,.craft-rules.yml,.craftsman-baseline.json) is denied instead of asked: in that mode an ask proceeds without the user seeing it, and one write of the global config could sethooks.disabledfor every later session. In the default mode it is still put to the user. If you run/craftsman:setupin bypass mode, edit the file yourself or approve it outside bypass mode. - The plugin’s own data is protected like its code: registry caches, session
state, session bindings and the
~/.claude/craftsman-*bridges. A forged registry cache had turned a deniedphpstan.neoninto an allowed one. A shell command can still write there (see SECURITY.md). - A registry that cannot be built refuses the write instead of judging it against an empty list of protected configs; an unwritable cache directory with no earlier cache compiles a private copy first.
- A skill’s
set-verifiedis refused after a failing test run, until a passing run clears it: the evidence a failure revoked can no longer be restored by asking. - Session bindings (
~/.<host>/craftsman/sessions/*.json) are swept after 7 days with the other session files; nothing removed them before. - A session started from another host’s shell carries both hosts’ ids; skills now resolve the innermost one (the most recent binding) instead of whichever host a fixed order named first.
- A gate is never written through a symlink: a symlinked gate file or gate directory is refused and its target left untouched. A first write honours the umask (0644 under 022) instead of the 0600 of a temporary file.
SECURITY.mdgains “Host limits that weaken the gate”: Grok’s fail-open hook timeout, Codex’s trust step, and shell-written files.
Fixed (test suite)
- A test whose
cdfails inside the plugin repository now stops, and no test may run a git write there (tests/lib/test-helpers.sh). With/tmpunwritable,tests/core/test-hostile-repo.shfell back to the repository and committed its fixtures on the current branch. Its work directory now comes frommktempunder$TMPDIR.
Documentation
- README and README.fr rewritten for adoption: one install block per host
(Claude Code, Codex, Grok, Hermes), a “first ten minutes” path that
provokes a refusal on purpose, and a host support table read from
hooks/host-capabilities.json. The Codex and Grok badges read their qualified version from that matrix, so they cannot drift from it. - New
docs/guides/codex-quickstart.mdanddocs/guides/grok-quickstart.md(install, verify, limits, uninstall), on the model of the Hermes one. - Every command now has a worked example:
agent-design,ci,loop,mlops,rag,scaffoldandspechad none.COMMANDS-QUICK-REF.mdsays who starts each command and links its example. tests/core/test-command-docs.shfails when a skill has no example, when a document names a command that does not exist, when the quick reference misses a command or its example, when the README’s invocation split disagrees with the skills’ frontmatter, and when a host that runs no plugin hook lacks its gate command in either README.
v4.11.0 - 2026-09-20
Changed
- The verification evidence is revoked whenever a failing command ENDS on a
test runner, not only when the runner is the whole command. A session that
ran
echo 1 > pytest.rc && ./bin/pytest -q, saw it exit 1 on a red suite, and keptverifiedtrue was measured on 2026-09-20;verifiedgates a push, so doubt revokes it. The session is still woken with “REGRESSED” only when the failure can be attributed to the runner alone; a compound failure revokes quietly and says how to grant the evidence again. If your habit is to chain a command before your test runner, expect to re-run the runner on its own to restore the evidence.
Added
-
Host adapters, one core (ADR-0029, campaign of 2026-09-15). The write gates read a Codex
apply_patch(V4A, multi-file, moves,@@anchors, End of File) before it lands and refuse the gate’s own configuration withdenywhere the host cannotask; test runs are decoded per host from the shapes the hosts actually send (hooks/lib/tool_result.py), and a host whose shell output carries no exit code grants and revokes nothing; every hook names its session from its payload (session_files_bind); the doctrine export owns one delimited block ofAGENTS.md; pack skills ship as regular files and learned skills may land in.agents/skills; agents export as Codex roles (craftsman-ci export --target codex-agents); the challenge skill collects its context withcraftsman-context review; the semantic review has a backend port (claude-cli,codex-cli,none) and each run row names its backend; a per-host event matrix (hooks/host-capabilities.json) and a host row in the healthcheck say what each host loads and observes; a Copilot adapter honours the documented contract, surfaces still to qualify; a fresh install from the built archive is tested; every handler carries astatusMessage. Fixtures undertests/fixtures/hosts/are captured from the real CLIs with their provenance (tests/fixtures/hosts/PROVENANCE.md), which also records what each host was seen NOT to do. -
Codex qualified by Codex (2026-09-20): a Codex session installed this branch natively (
codex plugin marketplace add, which reads the Claude marketplace manifest, thencodex plugin add) and reported what it found. Four defects fixed, each with a witness seen red first.modelwithoutprompt_idnames Codex and a string transcript path no longer names Claude Code: run through its app server, Codex 0.154.0 sends a REAL transcript path where a project hook of the same version sent null, so that session called itself claude-code and wrote Claude Code’s bridge. A shell withCODEX_SESSION_IDorCODEX_THREAD_IDnames Codex, socraftsman-healthcheckno longer reports “unknown host” about its own host, and the session-bridge row is read per host instead of judging every host by~/.claude. The rules store honoursTMPDIRand the plugin data directory before/tmp, which a sandboxed Codex could not write, turningmktemp: mkdtemp failedinto the refusal the model read. The Codex row of the capability matrix records twelve event kinds (Interrupt included), that installing and enabling a plugin does not trust its hooks, and that a background hook cannot force a continuation there. Two limits are now stated rather than implied (docs/reference/hooks.md, README): the gate judges the host’s write TOOLS, so a file written by a shell command the model runs is caught by CI and the pre-push gate, not before disk; and async delivery is the host’s, which Codex does not offer. -
A host’s hooks file is generated from the manifest, never typed twice:
craftsman-ci export --target grok-hooks(andcodex-hooks) writes the handlers with${CLAUDE_PLUGIN_ROOT}expanded and carried in each handler’s environment, the events that host does not fire dropped, and the conditional handlers that are Claude Code’s dropped. Grok needs it because it discovers a plugin’shooks/hooks.jsonand runs none of it; the generated file was replayed on the real CLI and gives the same refusals./craftsman:healthcheckgained anagent-rolesrow: on Codex it counts the craftsman roles in that host’s home and names the export command when there are none, since installing the plugin does not write them and, written, all twelve are offered to spawn_agent. README documents the native install on both hosts, including the steps installing does not perform (trusting the hooks, exporting the roles) and what each host does not offer. -
Grok 1.0.30 as a host, measured (audit of 2026-09-15): the write gates judge its
writeandsearch_replacetools (they exited 0 in silence on the captured envelope),host_detectnamesgrokfrom the envelope (workspaceRootand a lowercasehookEventName; it was read as Copilot because both carry atimestamp) and fromGROK_SESSION_ID, the capability matrix has agrokrow and the healthcheck describes any host from that row instead of a case per host, andrun_terminal_commandresults carryexit_code, so the verification loop is live there. The mirror now holds one table of host tool names to a kind of write (WRITE_TOOL_KINDS), and the pre-write gate no longer branches on a tool name. What Grok does NOT do is recorded too: it discovers a plugin’shooks/hooks.jsonand loads none of its handlers (headless, project plugin enabled;tests/fixtures/hosts/PROVENANCE.md), so on Grok the engine runs only from a project or global hooks file, and “compatible Grok” is not claimed. -
Hermes: a
git pushbetween two conclusions waits for the conclusion gate.adapters/hermes/pre-tool-call.shnow readsterminaland refuses a push (and a commit understrict) unlesspre-verify.sh’s last verdict is a pass on the tree that would be published; the verdict is recorded with that tree (terminal_gate.py,<git-dir>/craftsman-verdict), so a pass on one tree does not authorise another, and with no verdict at all the push waits. The matcher becomes^(terminal|write_file|patch)$.tests/adapters/test-hermes-terminal-gate.shcovers no verdict, a refused turn, a pass, a different tree, a pipeline,git -C, and the two strictness levels; every refusal was seen red before the branch existed.
Fixed
- The engine holds no list of languages, as CLAUDE.md has said since the
language registry: the
FileChangedmatcher carried a glob the event does not support (*.php|*.ts|*.tsx, so the hook never fired; it now watches the common source roots and the registry decides the language), the session banner found onlycomposer.jsonandpackage.json(it now reads every pack’sentry_markersand compares pack names), the codemap and the write mirror kept their own extension and marker lists (they read the registry), the metrics extractor took a language name in place of the declaredmetrics_dialect(it takes a dialect only, and the symfony pack declaresphp-like, which is what it always received), the engine kept the one list of rule ids it says it must not hold (relaxed_in_tests: trueis declared on the rule, compiled as the registry’s seventh column, and asked throughrule_relaxed_in_tests), and the PHP namespace reader and the Python AST checker lived inhooks/lib(each is in its pack).tests/core/test-no-language-literals.shscanshooks/andci/and fails on any language name or extension outside a comment: fourteen hits before, zero after, two of them kept on an explicit allowlist with a backlog line each. - A baseline row had two owners with two cadences: the structural mark
tightened on every green pass, the rule counts were written once by
craftsman-ci baselineand never moved. Debt that was paid kept its count, so the same debt coming back was reported as inherited on the very write the ratchet refused as a regression. A green pass now tightens the rule counts to what it observed (rule_baseline.py tighten), and agit mvkeeps the file’s mark instead of photographing the renamed path at whatever state the first write left it in. - Five session-state keys were read and written by nothing:
design_used(the design-pass exemption in the bias detector never fired; a/craftsman:designprompt now sets it),writes_count(the TaskCompleted evidence gate saw zero writes; it counts the session-writes file), andagent_invocations,team_type,completed_tasks(sessions.agents_spawnedheld[]on every row; it carries the agent types the subagent gate records). The README rows saying the plugin blocks a design decision now say it warns, which is what it does. ci/craftsman-ci.shexited 0 on a checkout missingrules-engine.sh,pack-loader.shorprecedence.sh: it scanned nothing and reported clean. The four libraries the gate is made of now load through one function that exits 2 naming the missing file; the standalone severity table and config parser, reachable only in that state, are gone.- Five published claims the code contradicted: the Level 2/3 budget
(15s per file, 30s per project, not
<2s),pre-push-verify.shwarns and does not gate, Levels 2 and 3 are off untiltrust_project_tools, all eighteenCRAFTSMAN_*switches are named in SECURITY.md, and the rules table indocs/ci-integration.md(13 of 66) is replaced by the export command that prints the live list.tests/core/test-doc-claims.shreads the code first and the document second. plugin.yaml, the Hermes manifest at the repository root, is now the fifth filescripts/bump-version.shtracks, so--checkandrelease-guard.shrefuse a release that leaves it behind. It was off the list, and 4.9.0 and 4.10.x shipped withhermes pluginsreporting 4.8.1. The manifest is at 4.10.3 again andtests/meta/test-release.shcarries the drift fixture.
v4.10.3 - 2026-09-14
A security patch and the tail of the whole-plugin review.
Fixed
never_ignorableheld only on machines with PyYAML. On a machine without it (the macOS system python, the CI runners) the fallback reader hands the stringtrue, the compiler compared it to the boolean, andSEC001,SEC002,SEC003compiled as ignorable: an ignore marker silenced a hardcoded secret on the hook, the pipeline and the Hermes write gate alike. The compiler now reads the YAML boolean whichever reader produced it, and the registry test compiles the manifest both ways.- A capability key the engine does not read is refused at compile time, with the key named on stderr, instead of being skipped silently.
- The ratchet is inert until a project opts in, and anchored on the file
rather than on the shell’s directory: a hook fired from
src/of a marked project no longer skips it. - The dogfood test walks every shipped script and judges each finding through the rules engine; seven functions over the structure thresholds in the Go and Rust packs and the GitHub adapter are split into named helpers.
v4.10.2 - 2026-09-14
The rest of the whole-plugin review’s blocking findings, each seen red first.
Fixed
- The Haiku verifier no longer grades itself. Any reply that was not the
findings token was read as CLEAN and closed every earlier finding on the
file as “fixed” with the file unchanged. Only the exact token is a clean
verdict now; anything else is
unavailableand closes nothing; a CLEAN closes a finding only when the file changed since the run that raised it. - A secret cannot be silenced with a marker, on any front-end.
SEC001,SEC002,SEC003are declarednever_ignorablein the manifest; every marker reader asks the registry first. - An Edit is judged before it lands, like a Write, and a gate that cannot run refuses the write instead of letting it land with a warning.
- Session state is one file per session. A session’s end no longer deletes another’s pending findings, and one project’s patterns no longer surface in another.
- The pre-write gate judges through the packs. Its detectors were a fork
(
Apphardcoded where the pack reads composer.json’s psr-4 root): anAcme\project passed pre-write and was refused post-write. The would-be file is laid out under a mirror of the workspace and judged by the same validators post-write and CI run. - One global configuration layer (
~/.claude/.craft-config.yml) for the hooks and CI alike, and none for the Hermes gates: a layer the same uid can write outside the gated turn must not shape a verdict. - Hermes: a gate file hidden from git is still the gate’s own, and a turn with no remote is scanned back to the root commit.
v4.10.1 - 2026-09-13
Added
- A finding can be judged right or wrong, and each rule’s acceptance is
reported (#44).
/craftsman:metricsreports acceptance per rule and proposes a relaxation under the threshold. A verdict is about a finding, not an outcome, so it lives in its ownverdictstable with two human writers: the reason spelled out in the suppression at the moment of the decision (craftsman-ignore: RULE (wrong: why)or(debt: why)), andmetrics_record_verdictfrom the review after the fact. No path writes a verdict the developer did not spell out. - Hermes: the write-time promise, opt-in (#21).
write_gate: onrefuses awrite_fileorpatchbefore it reaches disk on SEC001 (a hardcoded secret) and LAYER001 outside test paths. SEC002 and SEC003 stay at the conclusion gate: line-local regexes fire on the documented mitigations.
Fixed
pre-push-verify.shran on every Bash call.hooks.jsoncarried theiffilter besidematcher, where Claude Code ignores it. Moved onto the handler; the schema job refuses any other key at matcher level.- Documentation re-measured against the code. Agent count (12: 6 core plus 6 from the packs), ADR count (31), which skills the model may start (7 of 22), hook latency (measured, not “under 50ms”), analyser budgets (15s per file, 30s per project, not “under 2s”), pricing per model, the GitLab artifact shape, and a lesson that described a feature removed in ADR-0024.
v4.10.0 - 2026-09-13
Twenty-nine pull requests: the campaign that closed every open issue, then the fixes from a whole-plugin review by four reviewers crossed with the maintainer’s knowledge base on guardrails, evals and production feedback loops.
Fixed
- Every Haiku verification used to end the real session. The headless
claude -pverifier fired the plugin’s own SessionStart and SessionEnd hooks: the real session’s state was deleted on every verification, and 1460 of 1599 sessions in 30 days had no write at all. The correction loop was measuring its own verifier. Both hooks step aside under the guard the verifier already sets; proven on the real consumer. agent_hooks: falsenever reached a hook. Claude Code exports an option asCLAUDE_PLUGIN_OPTION_<KEY>with the key uppercased; every reader read the lowercase spelling. The README’s “turn it off withagent_hooks: false” is true from this release.- An approved instinct was written where Claude Code never looks.
.claude/skills/craftsman-learned/<rule>/is one level below the depth a project skill is loaded from.approvewrites to.claude/skills/learned-<rule>/and refuses any other depth. - A scoping decision is no longer recorded as a fix. A directory
.craft-rules.ymldemoting a rule, or a baseline mark holding it, counted asfixedand three such files made a candidate for a learned skill. Recordedscopedandoverriddennow, which the instinct gate reads as rejection. - The pre-write gate resolves severity through the rules engine. The
same file, rule and
.craft-rules.ymlgave “BLOCKED” before the write and nothing after it. One authority now; the advisory list it held is gone. - The gated party no longer reconfigures the gate. A Write to
.craft-rules.yml,.craft-config.yml,.craftsman-baseline.json,.claude/settings.jsonor the plugin’s own code passedconfig-protection.sh. The gate’s own files go to the user’s permission prompt; Claude Code’s settings and the installed plugin are denied. - The instinct gate counted directories, not files.
MIN_DISTINCT_FILEScompared directory globs; 23 fixes under one glob made no candidate.
Added
- An eval suite for the routing table (#48).
evals/: eight cases over the six model-invocable skills, a counter-example where no skill may fire, a discrimination case, graded bytool_usedandregexonly. Run withscripts/run-evals.sh, never fromtests/run-tests.sh: every case is a model call on your account. - Packs validate their files whatever the stack says (#35), the advisory
default is declared once in the manifest (#37), one brace walk for every
brace-delimited pack (#38),
--changed-onlyin CI (#50), per-environment latency ceilings, the structural ratchet re-taken once.
v4.9.0 - 2026-08-27
Added
-
Bias detection language cascade. Bias patterns moved out of the hook into one data file per language, loaded by a registry. English is the single curated language and keeps its direct warning; twelve others sit at one same tier behind it, carrying recall-oriented lexeme lists whose hits are not warnings. The hook prints an adjudication note and the model already reading your prompt, holding the whole session, either surfaces the warning in your language or drops the note silently. No second model, no subprocess, no network call on the prompt path. A false positive there is invisible, which is what makes high recall affordable, and adding a language is two data files and zero code. Language tags follow BCP 47, so
fr-CAorzh-Hantregister like any other; before this, such a file registered nothing while the hook still exited 0. -
Actionable verdicts. Measured on 18748 real violations: twelve rules fired 5624 times in 30 days and produced zero corrections, in either direction. Two causes, both fixed. A rule now names what it found, because the rule that named an identifier was acted on 167 times while the one that named nothing was acted on never. And both output paths now offer scoping before silencing: the blocking path had named suppression as the only way out, so a rule wrong for a whole directory was suppressed once per file instead of scoped once, and the advisory path carried no action at all.
-
scopedandopenoutcomes in the metrics. Scoping a rule was indistinguishable from giving up on it, and a verdict that reached the end of a session with no outcome was invisible. Both are now recorded, which is what makes the two fixes above measurable rather than merely plausible. -
Verifying the instrument, in the knowledge bundle. Five instrument failures were observed in one session and none was a defect in the code under test: a check that read a comment instead of the emitted message, a pattern that matched inside the corrected name and reported the fix as the defect, a fixture that never triggered the rule it tested, an all-time aggregate that described a problem solved a month earlier, and a verification whose output was piped away unread. Code is exercised constantly and fails loudly; an instrument is exercised once, silently, and its failure looks like success.
v4.8.1 - 2026-08-11
Changed
tests/core/test-config.shsplit into 21 section functions. The last whole-file span in the top three structural offenders: complexity 102 to 10, longest span 505 to 30 lines, 90 assertions before and after. Two oversized intermediate functions were blocked during the campaign and both were re-split instead of suppressed.
v4.8.0 - 2026-08-11
Changed
- Structural debt campaign, first production run of
/craftsman:loop. The two worst production offenders in the ratchet baseline were extracted under their marks with behaviour intact (full suite green at every iteration):hooks/session-metrics.shworst-span complexity 37 to 8 and longest span 131 to 16 lines (duration, write counter, violation counter and summary builder each became a function);hooks/lib/dispatch-context.shcomplexity 28 to 11 and longest span 97 to 27 (one emitter function per context section, early returns instead of nesting). Thefile_linesrises (131 to 149, 97 to 110) are the function boilerplate paid for it and are recorded in the baseline with that reason; every other mark tightened one-way.
v4.7.0 - 2026-08-11
Added
/craftsman:loop, a bounded verification loop. The repeat layer on top of the existing gates, with a stated bound rather than an open-ended retry.tests/core/test-gate-independence.sh. Auto mode becomes the default, and the test asserts each gate still decides on its own.- A context footprint guide. What the plugin injects per session, measured rather than estimated.
Changed
- The advisory Haiku layer respects the effort dial, so a low-effort turn no longer pays for a review it did not ask for.
/craftsman:teamoffers the native Workflow tool explicitly, and/craftsman:workflowdocuments how it maps onto the native/goal./craftsman:challengestates its position against the native reviewers instead of leaving the overlap implicit./craftsman:metricspublication is explicit, never a side effect.- Three skills removed:
agent-design,mlopsandrag. The plugin ships 19 skills.
v4.6.4 - 2026-08-10
Fixed
/craftsman:challengereturned nothing at all, one run in two. The skill forked into an agent capped at 20 turns. When the cap lands on a tool call the loop stops there and returns no text, and Claude Code substitutes the literal stringCommand completedfor the missing result. No error, no partial, no retry: 23 of the agent’s first 38 recorded runs ended that way. The review now runs in the main session and fans out to reviewer subagents when the scope is large.- The review could not see what it was reviewing. A forked skill carries neither the conversation nor the attachments. A review invoked with seven screenshots received seven
[Image #14]tokens and zero images. - The architect agent reviewed a tree in which the diff did not exist. It declared an isolated worktree, which is a clean checkout of a commit, so the uncommitted changes its injected
git diff HEADpointed at were absent from it.
Added
- Every agent that caps its turns now carries a delivery contract: emit the deliverable as soon as the evidence justifies it, reserve the last third of the budget for writing, name what was not covered, never end on a tool call. The cap was never the whole defect: an agent that keeps no budget for writing fails at any cap.
v4.6.3 - 2026-08-09
Fixed
- The deptrac adapter had never produced a single verdict. It called a formatter that exists in no version of deptrac. The call failed, the output was swallowed by
2>/dev/nulland|| true, andDEPTRAC001was never emitted once. Every layer violation this plugin ever reported came from the import regex. Nothing caught it because no test asserted that the analyser produced output, only that the code path ran. - A project could not configure the severity of a layer verdict. deptrac findings arrived under a code no
.craft-config.ymladdressed, soblock,warnandignoreall produced the same outcome.
v4.6.2 - 2026-08-09
Fixed
/craftsman:teamgated native teams on a tool Claude Code had removed, so every session silently degraded to parallel subagent dispatch.
Added
- Level precedence between validation levels. A rule a static analyser owns is deferred, never dropped: if the analyser times out, crashes, or is configured to ignore the rule, the regex finding is re-emitted. No verdict is not a clean verdict.
v4.6.0 - 2026-08-08
Added
- Packs own the rules they enforce, instead of a central export holding every rule for every stack.
v4.5.0 - 2026-08-08
Added
- Initialising a ratchet baseline requires a written reason, and records it. A scoped exemption with no stated cause is how a ratchet quietly stops ratcheting.
Fixed
- The structural metrics measured the gap between functions, not the functions.
v4.4.0 - 2026-08-08
Changed
- Packs declare the languages they cover, and the engine holds no list of its own. Breaking for pack authors.
Fixed
- The
pythonandbashpacks never ran in CI. The scan walked paths neither of them lives in. - One PHP file silenced the empty-gate guard for every other language.
v4.3.0 - 2026-07-29
Fixed
- The orchestrator told the model to run skills the model cannot run.
- The subagent quality gate validated nothing. Its header claimed it validated; it did not.
- A review could silently run a model tier below what the docs promise.
Every version, in full
The complete history, including the patch releases summarised here, is in CHANGELOG.md in the plugin repository.
v4.2.0 - 2026-07-28
An audit release. Every finding below was reproduced before it was fixed, and each carries a test that fails when the fix is removed. The recurring shape is one defect wearing different clothes: a control that reports success when it did not run, and a measurement that reports a number the code does not have.
Security
- A cloned repository could write anywhere the developer could.
ratchet.pywrote its baseline withwrite_text, which follows a symlink, so a repo shipping that path as a link to~/.claude/settings.jsonhad the target overwritten on the first edit. Writes now go through a sibling temp file and land withos.replace, which acts on the link and never on its target. - A cloned repository could switch the gates off.
hooks.disabledresolved from the project file first, so a clone could open its first session with every gate silent. A repository may tune what the gates check, never whether they run. - A psr-4 key could disarm the layer rules.
_layer_ns_regexescaped backslashes only, so a root such asA(pp\made every layer grep exit 2, which reads as “not this layer”. LAYER001 to LAYER003 stopped firing repository-wide, in the hook and in CI. - A file name could address the reviewer. Names from
git diffandtool_inputwere spliced verbatim into the Haiku review prompts. Names outside a plain-path charset are dropped, and the drop is reported. - A learned skill carried unsanitised text into the model’s context. A newline in
pattern_summaryorfile_patternopened a second YAML frontmatter block in the generated skill. Both are collapsed, capped and stripped of the fence character, and the skills destination is confined to the project or~/.claude.
Fixed
- CI reported a pass on a pipeline that inspected nothing. The default scan path was
src/, so a Laravelapp/, a monorepopackages/or a Next.jsapp/matched nothing and the run exited 0 withfiles_scanned: 0. Every common source root is scanned now, discovery is counted separately from scanning, and an empty discovery fails loudly. - Three blocking rules reported findings the code did not have. SH002 and WARN-SH001 counted braces without knowing which were shell, so a 14 line function holding an embedded python snippet was reported as 128. PY001 grepped raw lines, so a docstring beginning with an English article blocked writes on prose. SH001 inspected the first 20 lines only. All four measure from a proper scanner now.
- The exported doctrine covered 15 of 38 rules and described three of them wrong. PHP003 was documented as “private constructor plus factory” while it enforces “no public setter”; PHP004 as “no setters” while it enforces “no new DateTime()”; TS002 as “readonly by default” while it enforces “no default export”. A teammate on Codex read one rule and was blocked by another.
- The test suite trained the learning loop on its own fixtures.
tests/run-tests.shset noCLAUDE_PLUGIN_DATA, so four months of runs recorded their fixtures as production violations: 3704 of 15588 violations sat on a path this repository does not have. - Session counters were decorrelated from reality. SessionEnd re-queried the violations table over a window the length of the session, which counts every other session’s rows: 236 sessions in one day reported 16236 warnings against 1353 actually recorded.
- Level 2 and 3 could not run and said they were clean. Every analyser was capped at 2 seconds, below the cold start of all of them, and
|| trueflattened the timeout into success. - Rules vanished in silence without python3. Six checks shell out to it and each returned quietly, so the packs reported every file clean on a machine that has none.
sqlite3gets the same treatment in the metrics layer. - Whole classes of file were never validated.
post-write-checkallow-listed the characters a path may contain, so every Next.js App Router dynamic route, every route group and any accented path went unchecked. The rule is inverted to refuse only what is dangerous. - Bitbucket published a green PASSED on an unreadable report. The verdict now travels in the result field and its reason in the free-form details line.
metrics_project_hashhashed$PWD, filing one repository under a different project for every subdirectory worked from, andmetrics_file_patternrecorded absolute paths, including the developer’s home directory.
Added
adapters/, a third front-end over the same core.hooks/serves Claude Code,ci/craftsman-ci.shserves pipelines, andadapters/<host>/serves other agent runtimes. The first isadapters/hermes/: apre_verifyhook that refuses a conclusion rather than a write, because blocking a write assumes somebody can break the loop it creates, and an autonomous agent has nobody. It derives its scope from git rather than from the agent’s self-report.- Test paths degrade the rules whose premise is production code. LOC001, NEST001, PARAM001, GOD001, CTRL001, SEC001 and SEC002 become warnings under
tests/,spec/,__tests__/,__mocks__/,fixtures/andfactories/. A long setup and a credential in a fixture are the normal shape of a test, and blocking on them is what teaches a developer to reach forcraftsman-ignore. - A
sourcecolumn onviolationsandcorrections, so the next contamination is aDELETErather than an archaeology exercise.
v4.1.2 - 2026-07-27
CI duration, which on this project is CI cost.
Changed
- Adaptive polling in the portable timeout. The fallback slept a full second per call while almost every call finishes in milliseconds, adding 163s to the macOS job. It now polls at 50ms, backing off to 250ms after a second and 1s after ten. Measured over 50 fast calls: 972ms per call before, 64ms after.
- One timeout implementation instead of three.
hooks/lib/portable-timeout.shis the single source; the static-analysis dispatcher, the test helpers and the test runner all source it. - The workflow cancels superseded runs. Without a
concurrencyblock, pushing twice ran two full matrices for one answer that mattered.
Added
- A Python floor job. The test matrix used 3.12 on both runners, so it could not see a module that only parses on 3.10+. A separate job imports every hook library under 3.9.
- The runtime floor is stated in both READMEs: Python 3.9, no GNU coreutils required.
v4.1.1 - 2026-07-27
Every failure here was a “works on my machine” dependency the repository had been carrying unnoticed.
Fixed
- Level 2 and Level 3 static analysis never ran on a stock macOS. All five analyser invocations were wrapped in
timeout, which comes from GNU coreutils and is not installed by default on macOS. The call failed with 127, the|| trueswallowed it, and the plugin reported a clean gate having run nothing.sa_timeoutnow usestimeoutorgtimeoutwhen present and falls back to a background job with a watchdog. - The instinct pipeline was dead on a stock macOS.
hooks/lib/instincts.pyannotated a parameter with syntax Python evaluates at runtime from 3.10, and/usr/bin/python3on a Mac without homebrew is 3.9. Fixed withfrom __future__ import annotations. sed -i ''is BSD syntax and GNU sed reads the empty string as the script. Fixed in the pack validator test, which failed on Linux, and inbump-version.sh, which would have silently skipped a version bump for any contributor on Linux.
v4.1.0 - 2026-07-27
Security fixes, rule calibration, and twenty test suites the runner never called.
Security
- The CI gate no longer fails open on a malformed report. It parsed the report with stderr discarded, so a report that was not valid JSON produced an empty summary and exit 0. It now fails closed.
- Command injection through a channel number closed. Config values landed in an arithmetic expansion where bash evaluates an array subscript as a command. Refused at the config boundary and again at each sink.
- Grep flag injection closed at three sites: a pattern beginning with
-was read as an option. - secrets-scan fails closed. It reported “no secrets found” for every condition that makes a scan produce nothing. It now proves it can enumerate tracked files, read their content and read history before any empty result counts as clean.
- Six credential shapes added to the scan, and every
.envsuffix rather than only.localand.production. hotspot_analysis.pybounded. A 400MB file with no newlines took 952MB of RSS. It now shares the write-time gate’s 2MB cap.
Changed
TS002,TS003andPHP003default to advisory. Each has legitimate exceptions a regex cannot see, and blocking a write on those only teaches suppression.PHP002skips Doctrine entities. A proxy extends the entity, so a final entity breaks lazy loading: the rule was asking Symfony projects to break their own persistence layer.TS002skips files a framework resolves by default export (page,layout,route,middleware, and others), and both TS rules now take a line-levelcraftsman-ignore.- The layer rules read the project’s root namespace from
composer.json. They matchedApp\Domainliterally, so every project that renamed its root namespace passed all three layer rules by construction.
Fixed
- The CI quality gate no longer hangs. Severity resolution walked from the file’s directory up to the project root, but both stop conditions are absolute paths and CI is handed relative ones.
- CI agrees with the hook on
ignore, and resolves severity per file so a directory-level.craft-rules.ymlapplies in the pipeline as it does in the hooks. consolidate-metrics.shreports its failures instead of reading a locked source as “0 rows to merge”.pack_sync_symlinksonly removes what a pack put there.
Testing
- Twenty test suites the runner never called are now wired in, including the entire hostile-repository security suite. The suite reported 213 passing while a third of the files on disk sat unexecuted.
test-runner-integrity.shnow fails when a test function is defined and not called. - A failing subtest says why. All 26 wrappers used to discard their output.
- Guard tests can no longer pass against deleted code. Every absence assertion is gated on a positive control only a live tool can satisfy.
v4.0.1 - 2026-07-27
Corrects two settings that did not do what the repository believed.
Added
- Per-task model tiering, actually implemented. Every skill declares the cheapest model that can do its job (
haikufor mechanical work,sonnetfor bounded pattern application,opusfor judgment spanning files) alongside its effort level. The tiering had been documented since v1 but never implemented: no skill declared a model, so every skill ran on whatever the session happened to be set to. Tiers are aliases rather than pinned model ids, so they follow model releases and a whole tier can be remapped withANTHROPIC_DEFAULT_*_MODEL.
Fixed
effortused values Claude Code does not recognise. Eleven of twenty-one skills declaredquickorheavy, a project convention that predates the key.- Skills reference was missing
healthcheck,legacyandworkflow. The table is now generated from the frontmatter it documents. - Hooks reference documented 8 of the 13 wired events, omitting the
TaskCompletedevidence gate. - Installation and advanced guides still walked users through the
knowledge-ragMCP server, removed in 4.0.0. - CI validators required frontmatter keys Claude Code treats as optional, and read keys from the whole file so a YAML sample inside a skill body was mistaken for a declared field.
v4.0.0 - 2026-07-26
A clean break to a native-first, self-learning, security-hardened plugin. Requires Claude Code 2.1.218 or later. Rationale: ADRs 0016 to 0027.
Added
- Structural ratchet. A committed
.craftsman-baseline.jsonholds each file’s structural high-water mark (approximated complexity, file lines, longest function, import fan-out, suppression count). A file you touch may improve or stay equal, never regress. The mark tightens automatically on a green pass and only loosens through a documentedcraftsman-ignore, itself counted and ratcheted. The hook andcraftsman-cirun the identical check, and CI never writes the baseline. Untouched files and relaxed directories are never evaluated, so legacy is not punished for debt it already had. Ships advisory: setRATCHET001: blockto make it a hard gate. - Correction learning closes the loop. Recurring fixed corrections (3 or more across 3 or more files) become candidate instincts;
/craftsman:metricssurfaces them for human review; approval generates a learned skill with provenance. Instincts stay project-scoped until the same rule is approved in 2 or more projects. Promotion is never automatic. - Adversarial design panel. Three headless contradictors (YAGNI, invariants and aggregate boundaries, feasibility) attack the design during
/craftsman:designphase 2, before any code exists. Every objection must land in a retained or dismissed table. - Persistence craft. New knowledge pillar plus rules verified in hooks and CI: LAYER004 (raw SQL or a database client inside Domain), DB001 (
SELECT *), DB002 (migration withoutdown()), DB003 (query inside a loop). - Security rules SEC001 to SEC003: hardcoded secrets, dynamic eval, and SQL built by concatenation, with doctrine routed automatically when one blocks.
- Knowledge as an OKF bundle. Every concept carries Open Knowledge Format v0.2 frontmatter, and a deterministic lookup routes a rule or tag to the concepts explaining it. Zero embeddings, zero index, zero external service.
- Level 1.5 semantic validation.
.lsp.jsonactivates a language server only when the binary is already installed on the machine. - Deterministic verification loop. A failing test run revokes verification evidence and wakes the session on regression; a
TaskCompletedgate requires evidence before a task can be marked complete. - Situational onboarding and guided mode. Project init observes the repository, then asks at most four plain-language questions. With
guided: true, every block explains its doctrine. - Living legacy audit.
LEGACY-AUDIT.mdis committed and diffed between runs, with a mandatory “looks bad but is actually fine” section. Nothing is ever installed for you. - Outcome contracts. Every skill states its outcome, its done-when condition, and the evidence that proves it. The validator enforces all three.
- Cross-harness doctrine export.
craftsman-ci exportrenders the active rules asAGENTS.md,.cursor/rules/craftsman.mdcand.github/copilot-instructions.md. - Local dashboard.
/craftsman:metrics --dashboardaggregates every tracked repository into a self-contained page served on localhost. - Context budgets and kill switches. Every injection is capped and every hook individually disableable.
Changed
- All workflows are skills.
commands/*.mdbecameskills/<name>/SKILL.md. User invocations are unchanged:/craftsman:designstill works exactly as before. - Semantic verification is headless. DDD verification moved from Stop (where it was dead) to PostToolUse and runs in a Haiku subprocess. Zero main-context cost, and
agent_hooks: falsestill disables everything.
Security
Hardened against a hostile repository: opening or editing one must not execute its code. Each fix has a test in tests/core/test-hostile-repo.sh that reproduces the attack and asserts it fails.
- Level 2 static analysis is off by default (breaking). The gate resolved and executed
vendor/bin/phpstanandnode_modules/.bin/eslintfrom the working directory, so a shell script planted at that path in a cloned repository ran arbitrary code on the first PHP edit. Running a project’s analysers means running its code, so it is now consent: settrust_project_tools: truein your own global config. A repository cannot grant it to itself. - External packs are declared by the machine owner only. Their validators are sourced as shell code, so reading that path from a project config meant opening a cloned repository executed its code at session start.
- No Python source splicing in the GitLab adapter: an attacker-chosen filename reached the source text of a
python3 -ccall. - Rule ids are constrained. They are YAML keys from repository-controlled config and were used raw as filenames.
- Dashboard output is escaped and the server answers for exactly one file.
- Symlinks are refused. Git tracks symlinks as real entries, so
src/Thing.phppointing at~/.ssh/id_rsais a legal commit and the audit followed it. - Reads are bounded. Measuring a 94MB file cost 506MB of memory, and a size cap alone does not help because a symlink to
/dev/zeroreports size 0. - Verification verdicts are parsed by shape before reaching the session. The headless verifiers read files the plugin did not write, and forwarding their reply verbatim is an indirect prompt-injection path.
- Shipped CI template hardened: actions pinned to verified commit SHAs, least-privilege permissions, anchored paths.
Removed
commands/flat command files,output-styles/, and the v3 agent hook wrappers.- The
knowledge-ragMCP server, its skill, the Ollama dependency, and 115MB of dependencies. Superseded by the OKF bundle. - Support for Claude Code below 2.1.218.
v3.9.0 - 2026-07-14
Added
- Global config fallback.
~/.claude/.craft-config.ymlwritten by/craftsman:setupis now actually read by hooks. Resolution order: project config, then plugin option env vars, then the global file, then hardcoded defaults.
Fixed
- CI executable-bit check no longer fails on sourced libraries.
bump-version.shexits 1 when a tracked file matches neither the old nor the new version, instead of printing a soft warning that let a file silently drift two releases behind.
v3.8.0 - 2026-07-14
Added
- Config protection hook. Blocks writes to single-purpose linter and architecture config files, so an agent cannot silently loosen a rule instead of fixing the flagged code. Multi-purpose files (
pyproject.toml,package.json) are intentionally excluded. - Hook profiles. Session-level opt-out of secondary and costed hooks. The core quality gate, bias detection and session bookkeeping deliberately do not support it.
- Security invariant tests. A sandbox and witness-marker suite proving the protection hooks never execute injected code, never touch files outside their contract, and fail open on malformed stdin.
- Prompt injection defense baseline: external content relayed by hooks is data, never instructions.
- French README, FAQ, migration guide, and troubleshooting guide.
Changed
- CI hardening: all actions pinned by commit SHA, least-privilege permissions, dynamic script discovery, and the test suite running on an Ubuntu plus macOS matrix.
v3.7.0 - 2026-07-06
Added
/craftsman:legacy. Four modes for regaining control of an inherited codebase:audit(rank hotspots, produceLEGACY-AUDIT.md),cover(characterization net before touching anything),untangle(break dependencies),migrate(strangler fig). Consumes existing analysis-tool output via--frominstead of recomputing a weaker signal.legacy-surgeonagent and thelegacy-takeoverteam template.- Zero-dependency hotspot ranking (churn times complexity), as a fallback for teams with no external analysis tool, not a competitor to one.
- Legacy and refactoring knowledge pillars: characterization testing, strangler fig, Mikado method, code smells, communicating technical debt.
v2.6.0 - 2026-03-29
Pack ecosystem expansion.
Added
- Pack validation script validating pack structure, manifest and hooks before publishing.
- External pack support loading community packs from arbitrary paths.
- Pack creation guide with examples.
- Pack skeletons for Go, Rust and Python, ready to fork.
/craftsman:scaffold packgenerates a pack skeleton interactively.
v2.5.0 - 2026-03-29
Knowledge-RAG MCP server migration. (This subsystem was later removed in v4.0.0, replaced by the OKF knowledge bundle.)
Added
- Knowledge-RAG MCP server moved to a standalone directory, with stdio and HTTP transport.
/craftsman:scaffold api-resourcefor API Platform 4 resources.- Pack-specific test suites, and CI pack-loader integration.
v2.4.0 - 2026-03-29
Core plus pack architecture.
Added
- Pack loader reading pack manifests and registering agents, commands and rules.
- API craftsman agent for API Platform 4 resources.
- Unified scaffold command replacing the separate entity, usecase, component and hook commands.
v2.3.0 - 2026-03-29
Distribution optimization.
Added
- Distribution ignore list, bringing the plugin under 1 MB.
- Startup dependency check for Python 3, jq and SQLite.
- Agent hooks opt-out.
Fixed
- Knowledge base is lazy-loaded instead of read at startup.
v2.2.0 - 2026-03-29
Security and quality.
Security
- All database writes use parameterized queries.
- Hook entries in
.claude/settings.jsonare schema-validated at startup.
Added
- Atomic commit enforcement: the Stop hook warns above 15 staged files.
- Monorepo sampling: above 100 changed files, a representative sample per directory is validated instead of every file.
v2.1.0 - 2026-03-29
Rules engine and CI adapters.
Added
- Custom rule engine with 3-level inheritance (global, project, directory).
- Short-form rules and long-form custom rules with regex and per-language targeting.
- CI adapter system for 4 providers: GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins.
/craftsman:cito export CI config and check status.- Circuit breaker with 3 states, protecting the plugin’s own external service calls.
- Pack template variants.
v2.0.0 - 2026-03-28
Teams and onboarding.
Added
- Teams system with built-in templates for code review, feature implementation and security audit.
- Session onboarding with config summary and cost estimate.
- Pre-push verification on the Stop hook.
v1.5.0 - 2026-03-28
Agent fleet expansion.
Added
- 5 specialized core agents:
architect,doc-writer,security-pentester,ui-ux-director,team-lead. - Parallel agent execution via
/craftsman:parallel. - Pack agents:
backend-craftsman(Symfony),frontend-craftsman(React),ai-engineer(AI/ML).
v1.4.0 - 2026-03-28
Sentry integration.
Added
- Sentry context fetching with a channel lifecycle and automatic cleanup.
v1.3.0 - 2026-03-28
Semantic intelligence and correction learning.
Added
- Agent hooks analyzing code changes for DDD and Clean Architecture compliance.
- Correction learning system: SQLite tracks when Claude corrects a violation.
- Metrics database, the foundation for 7-day and 30-day trend analysis.
v1.2.0 - 2026-03-28
Validation levels and metrics.
Added
- Multi-level code validation: regex (under 50ms), static analysis, architecture.
- Blocking hooks: the write is refused until the violation is fixed.
- Pre-write validation, so the check runs before the write rather than after.
/craftsman:metricsquality dashboard.craftsman-ignoreinline suppression syntax.
v1.1.1 - 2025-02-06
Added
/craftsman:setupimplementation, with a DISC mini-assessment in the wizard.
v1.1.0 - 2025-02-05
Fixed
- Version synchronization between manifest files.
v1.0.0 - 2025-02-04
Initial release.
Added
- 20 commands: 10 core, 2 PHP, 2 TypeScript, 4 AI/ML, 1 CI, 1 utility.
- 5 specialized agents.
- Hooks system: PreToolUse, PostToolUse and Stop validation pipeline.
- Knowledge base covering DDD, Clean Architecture, TDD and framework specifics.
- Optional Knowledge-RAG MCP server via Ollama for vector search.
- 3 built-in packs: symfony, react, ai-ml.