You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A project config write refuses a link at any folder on its way
A project config write now refuses a link at any folder on its way. Nothing is removed from the plugin, but one case that worked before is now refused: a .claude (or .claude/coal) link inside the project that a config write went through. The installed plugin moved: scripts/lib/repo-fs.mjs and its plugin/ twin (git diff --stat daeb04f HEAD -- plugin/ lists only that and the version in plugin.json); the advice change is in scripts/configure.mjs, which is not part of the installed plugin.
Security
configure.mjs's project-config write (writeRepoFile) now refuses a link at any folder on its way, wherever it points. It walks the folders from the project root down to the config's folder, checking each one before it makes the next and before the file is written. A link or junction at any of them is refused, including one that resolves inside the project: before this change a .claude junction to <project>/inner passed the containment check and the config landed at <project>/inner/coal/coalwash.json. A missing folder is made one at a time with a plain make and checked again. The global config write (--global) is unchanged. The walk is the one CoalLedger's mkdirRepoParents uses (CWK2-048). The race named below is still open. — test: scripts/configure.test.mjs, CWK2-048: a project config write whose `.claude` links to a folder INSIDE the project is REFUSED, and nothing is made in that folder; scripts/lib/repo-fs.test.mjs, the eight tests whose names start CWK2-048:, among them CWK2-048: a project write through a `.claude` link that points INSIDE the project is refused, and nothing is made at the link target and CWK2-048: a project write through a `.claude/coal` link that points INSIDE the project is refused, and nothing is written at the link target (the directory-symlink leg skips visibly where a symlink cannot be made, and belongs to the POSIX CI legs).
Fixed
When a project config write is refused because of a folder on the way to the config, configure.mjs now names that folder, with advice that fits why it was refused. A link or junction there, wherever it points (a dangling one included), or a file where a folder belongs: "Replace the link or file at with a real folder (or remove it) and re-run." A folder CoalWash could not make or read: "Check that can be made and read (the permissions on it and on the folder above it, or another program holding it) and re-run." Three rarer causes have their own advice: a folder that was gone right after it was made, one that leads outside the project with no link CoalWash can see, and one that is lexically outside the project. Before, every one of these told the user to replace the config file, which does not fix the refusal. A refusal of the config file itself keeps its advice ("Replace it with a regular file inside the project (or remove it) and re-run."). The advice is chosen from the refusal's own kind, never from its message text. — test: scripts/configure.test.mjs, CWK2-048: a project config write whose `.claude` links to a folder INSIDE the project is REFUSED, and nothing is made in that folder, R86-1: a project config write whose `.claude` links OUTSIDE the project names that link in the advice, and nothing lands outside, R86-1: a project config write whose `.claude` is a DANGLING link names that link in the advice and R86-1: a folder CoalWash cannot make on the way (an ACL denies adding a subfolder) is named with permission advice, never link advice; scripts/lib/repo-fs.test.mjs, R86-1: a `.claude` link that resolves OUTSIDE the project is refused carrying that link as the folder (kind link); the string reason is unchanged, R86-1: a DANGLING `.claude` link is refused carrying that link as the folder (kind link); the string reason is unchanged and R86-1: the walk's refusals carry their kind: a link and a file in the way, and a sibling outside the root (the permission leg uses an ACL deny and skips visibly where it cannot be set or does not stop the account).
Changed
Named residual: configure.mjs's project-config write has a check-to-write window.writeRepoFile checks the target's nearest existing folder, then walks every folder from the project root down and refuses a link or junction at any of them, before it makes the missing folders one at a time and writes; a hostile local process that swaps a folder it has already passed for a junction to outside can still land the config file outside the project (measured on Windows with an instrumented swap; the per-component walk narrows the window and does not close it). A link planted ahead of time is refused before anything is made, wherever it points; the SECURITY.md "Not closed" list (item 5 of the 2026-10-15 record) names the race.
Contributor-facing: six files re-copied by blob from the .github canon at bea2b3d.scripts/lib/wave-run.mjsb60827c6, scripts/lib/wave-run.test.mjsbccbaa58, scripts/verify-release-shape.mjs270e43d1, scripts/verify-release-shape.test.mjs7c2b35c4, scripts/release-notes.test.mjsf9dde432 and scripts/lib/git-env-census.test.mjsc6b301cd. They carry: the release-shape check tolerates only GitHub's trailing newline on a title or body (a leading byte now fails); the wave runner's usage and refusal text name the per-test clock (--test-timeout) against the per-file clock (--file-clock-ms); every runWaves child in the wave-runner test runs on a sandboxed environment; the release-notes test tears down with the waiting removeTree loop. NOTICE also joins the push paths-ignore of ci.yml, codeql.yml and scorecard.yml. Dev tooling and CI; none of it is in the installed plugin.
Contributor-facing: three more canon files re-copied by blob from the .github canon at bea2b3d, and two pinning tests.scripts/lib/release-shape.mjs7a93e446 (an ## [Unreleased] top entry is named by its own rule message before the date is read), scripts/lib/release-shape.test.mjs87895ac2 and .github/workflows/create-release.yml4060b602 (package-manager-cache: false on its setup-node step, so no restored cache reaches the job that holds contents: write). In scripts/lib/repo-fs.test.mjs two tests pin the write walk: CWK2-048 L83-2: a folder on ANOTHER DRIVE is outside the project root, refused by name, and nothing is made (probed: path.relative yields an absolute path) and CWK2-048 L83-2: a project root REACHED THROUGH A LINK still takes a write at the root itself (the root is never walked). Dev tooling and CI; none of it is in the installed plugin.
Docs: NOTICE conformed to the org template and a CodeRabbit badge added to the README.NOTICE is now the org template's five lines (name, copyright holder and year, the series line, the licence). The README badge row gains the CodeRabbit pull-request-reviews badge.
Contributor-facing: the org canon's front-door link check is adopted. Four files copied by blob from the .github canon: scripts/front-door-links.mjs3793a056, scripts/lib/front-door-links.mjs7df59db6 and scripts/front-door-links.test.mjs3cff2b30 at canon commit fd40327, and .github/workflows/front-door-links.yml7c582bd7 (the blob the canon has carried since commit efd2c63, which moves the actions/setup-node pin to v7.1.0; the workflow first came in as db4173d4 from fd40327). The check fetches every outward http(s) link of the named front-door markdown files (fenced and inline code excluded) and fails on one that does not answer at the page it names; the workflow runs it on every change to a front door and on a weekly clock. Dev tooling; none of it is in the installed plugin (the engine joins DEV_ONLY_LIBS).
Forward note on the [1.12.2] entry (a released entry is not edited). Its exec-form bullet says that on Windows without Git for Windows the old shell-form hook ran under PowerShell, "where ${CLAUDE_PLUGIN_ROOT} expands to nothing". That states a mechanism as fact. What was measured was a probe that handed PowerShell the old literal string with Git removed from PATH; Claude Code's own no-Git fallback was not run (that bullet's own Limit line says so). Claude Code's hooks reference (code.claude.com/docs/en/hooks, read whole on 2026-10-16) says that Claude Code rewrites the ${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT} and ${CLAUDE_PLUGIN_DATA} placeholders in a PowerShell shell-form command to PowerShell's ${env:NAME} form, so the placeholder works inside double-quoted strings but not inside single-quoted ones. CoalWash's hook string before the fix (hooks/hooks.json at e7d49c9^, line 4) was double-quoted, so that reference, as read on 2026-10-16, contradicts the [1.12.2] sentence "expands to nothing" for that string. The reference does not say since which Claude Code version the rewrite exists, so it may postdate 2.1.299, and this entry does not settle what 2.1.299 did. The exec form stays: it needs no shell either way.
Forward note on the [1.12.5] entry (a released entry is not edited). Its opening paragraph says "Nothing a user could do before is taken away." That is not exactly true: its own Fixed bullet records that a hard link at a restored file's path was replaced before (ok: true), and estate-restore --to now refuses it and leaves it in place. The refusal is the intended narrowing.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
A project config write refuses a link at any folder on its way
A project config write now refuses a link at any folder on its way. Nothing is removed from the plugin, but one case that worked before is now refused: a
.claude(or.claude/coal) link inside the project that a config write went through. The installed plugin moved:scripts/lib/repo-fs.mjsand itsplugin/twin (git diff --stat daeb04f HEAD -- plugin/lists only that and the version inplugin.json); the advice change is inscripts/configure.mjs, which is not part of the installed plugin.Security
configure.mjs's project-config write (writeRepoFile) now refuses a link at any folder on its way, wherever it points. It walks the folders from the project root down to the config's folder, checking each one before it makes the next and before the file is written. A link or junction at any of them is refused, including one that resolves inside the project: before this change a.claudejunction to<project>/innerpassed the containment check and the config landed at<project>/inner/coal/coalwash.json. A missing folder is made one at a time with a plain make and checked again. The global config write (--global) is unchanged. The walk is the one CoalLedger'smkdirRepoParentsuses (CWK2-048). The race named below is still open. — test:scripts/configure.test.mjs,CWK2-048: a project config write whose `.claude` links to a folder INSIDE the project is REFUSED, and nothing is made in that folder;scripts/lib/repo-fs.test.mjs, the eight tests whose names startCWK2-048:, among themCWK2-048: a project write through a `.claude` link that points INSIDE the project is refused, and nothing is made at the link targetandCWK2-048: a project write through a `.claude/coal` link that points INSIDE the project is refused, and nothing is written at the link target(the directory-symlink leg skips visibly where a symlink cannot be made, and belongs to the POSIX CI legs).Fixed
configure.mjsnow names that folder, with advice that fits why it was refused. A link or junction there, wherever it points (a dangling one included), or a file where a folder belongs: "Replace the link or file at with a real folder (or remove it) and re-run." A folder CoalWash could not make or read: "Check that can be made and read (the permissions on it and on the folder above it, or another program holding it) and re-run." Three rarer causes have their own advice: a folder that was gone right after it was made, one that leads outside the project with no link CoalWash can see, and one that is lexically outside the project. Before, every one of these told the user to replace the config file, which does not fix the refusal. A refusal of the config file itself keeps its advice ("Replace it with a regular file inside the project (or remove it) and re-run."). The advice is chosen from the refusal's own kind, never from its message text. — test:scripts/configure.test.mjs,CWK2-048: a project config write whose `.claude` links to a folder INSIDE the project is REFUSED, and nothing is made in that folder,R86-1: a project config write whose `.claude` links OUTSIDE the project names that link in the advice, and nothing lands outside,R86-1: a project config write whose `.claude` is a DANGLING link names that link in the adviceandR86-1: a folder CoalWash cannot make on the way (an ACL denies adding a subfolder) is named with permission advice, never link advice;scripts/lib/repo-fs.test.mjs,R86-1: a `.claude` link that resolves OUTSIDE the project is refused carrying that link as the folder (kind link); the string reason is unchanged,R86-1: a DANGLING `.claude` link is refused carrying that link as the folder (kind link); the string reason is unchangedandR86-1: the walk's refusals carry their kind: a link and a file in the way, and a sibling outside the root(the permission leg uses an ACL deny and skips visibly where it cannot be set or does not stop the account).Changed
configure.mjs's project-config write has a check-to-write window.writeRepoFilechecks the target's nearest existing folder, then walks every folder from the project root down and refuses a link or junction at any of them, before it makes the missing folders one at a time and writes; a hostile local process that swaps a folder it has already passed for a junction to outside can still land the config file outside the project (measured on Windows with an instrumented swap; the per-component walk narrows the window and does not close it). A link planted ahead of time is refused before anything is made, wherever it points; theSECURITY.md"Not closed" list (item 5 of the 2026-10-15 record) names the race..githubcanon atbea2b3d.scripts/lib/wave-run.mjsb60827c6,scripts/lib/wave-run.test.mjsbccbaa58,scripts/verify-release-shape.mjs270e43d1,scripts/verify-release-shape.test.mjs7c2b35c4,scripts/release-notes.test.mjsf9dde432andscripts/lib/git-env-census.test.mjsc6b301cd. They carry: the release-shape check tolerates only GitHub's trailing newline on a title or body (a leading byte now fails); the wave runner's usage and refusal text name the per-test clock (--test-timeout) against the per-file clock (--file-clock-ms); everyrunWaveschild in the wave-runner test runs on a sandboxed environment; the release-notes test tears down with the waitingremoveTreeloop.NOTICEalso joins the pushpaths-ignoreofci.yml,codeql.ymlandscorecard.yml. Dev tooling and CI; none of it is in the installed plugin..githubcanon atbea2b3d, and two pinning tests.scripts/lib/release-shape.mjs7a93e446(an## [Unreleased]top entry is named by its own rule message before the date is read),scripts/lib/release-shape.test.mjs87895ac2and.github/workflows/create-release.yml4060b602(package-manager-cache: falseon its setup-node step, so no restored cache reaches the job that holdscontents: write). Inscripts/lib/repo-fs.test.mjstwo tests pin the write walk:CWK2-048 L83-2: a folder on ANOTHER DRIVE is outside the project root, refused by name, and nothing is made (probed: path.relative yields an absolute path)andCWK2-048 L83-2: a project root REACHED THROUGH A LINK still takes a write at the root itself (the root is never walked). Dev tooling and CI; none of it is in the installed plugin.NOTICEconformed to the org template and a CodeRabbit badge added to the README.NOTICEis now the org template's five lines (name, copyright holder and year, the series line, the licence). The README badge row gains the CodeRabbit pull-request-reviews badge..githubcanon:scripts/front-door-links.mjs3793a056,scripts/lib/front-door-links.mjs7df59db6andscripts/front-door-links.test.mjs3cff2b30at canon commitfd40327, and.github/workflows/front-door-links.yml7c582bd7(the blob the canon has carried since commitefd2c63, which moves theactions/setup-nodepin to v7.1.0; the workflow first came in asdb4173d4fromfd40327). The check fetches every outward http(s) link of the named front-door markdown files (fenced and inline code excluded) and fails on one that does not answer at the page it names; the workflow runs it on every change to a front door and on a weekly clock. Dev tooling; none of it is in the installed plugin (the engine joinsDEV_ONLY_LIBS).[1.12.2]entry (a released entry is not edited). Its exec-form bullet says that on Windows without Git for Windows the old shell-form hook ran under PowerShell, "where${CLAUDE_PLUGIN_ROOT}expands to nothing". That states a mechanism as fact. What was measured was a probe that handed PowerShell the old literal string with Git removed from PATH; Claude Code's own no-Git fallback was not run (that bullet's own Limit line says so). Claude Code's hooks reference (code.claude.com/docs/en/hooks, read whole on 2026-10-16) says that Claude Code rewrites the${CLAUDE_PROJECT_DIR},${CLAUDE_PLUGIN_ROOT}and${CLAUDE_PLUGIN_DATA}placeholders in a PowerShell shell-form command to PowerShell's${env:NAME}form, so the placeholder works inside double-quoted strings but not inside single-quoted ones. CoalWash's hook string before the fix (hooks/hooks.jsonate7d49c9^, line 4) was double-quoted, so that reference, as read on 2026-10-16, contradicts the[1.12.2]sentence "expands to nothing" for that string. The reference does not say since which Claude Code version the rewrite exists, so it may postdate 2.1.299, and this entry does not settle what 2.1.299 did. The exec form stays: it needs no shell either way.[1.12.5]entry (a released entry is not edited). Its opening paragraph says "Nothing a user could do before is taken away." That is not exactly true: its own Fixed bullet records that a hard link at a restored file's path was replaced before (ok: true), andestate-restore --tonow refuses it and leaves it in place. The refusal is the intended narrowing.Release page · CoalWash
All reactions