Skip to content

Releases: TheColliery/CoalMine

v3.20.2 - a link planted in a cloned repository can no longer crash the hooks or make install and configure write outside it

Choose a tag to compare

@github-actions github-actions released this 24 Sep 10:17
v3.20.2
c43fdc1

A link planted in a cloned repository can no longer crash the hooks or make install and configure write outside it.

Security

A cloned repository is untrusted input, and three defects let one act on your machine through a planted
symbolic link (a junction on Windows), FIFO or device file. Every release from 1.0.0 (the first
release, untagged: its heading below is dated 2026-06-09) through v3.20.1 is affected
; the
range comes from a walk of versions (plugin.json history, these headings and the tags), not of tags alone.
The per-defect first version and the full advisory are in
SECURITY.md. No CVE id is claimed; none exists. Found by a blind
automated security review (2026-09-24).

  • CWK-137 (1 of 3) — the hooks read repo-derived paths with no bound. A link to /dev/zero at
    AGENTS.md, MEMORY.md, README.md, a rules file or the project coalmine.json crashed the hook
    (std::bad_alloc, exit 134, measured on Linux under a 3 GB address-space cap); a FIFO at any of those
    paths, or a .claude/rules link to /, made the hook never return. The same unbounded read sat in
    verify.mjs <target> and its manifest check. Reads now go through readRepoFileBounded, in the three
    hooks (hooks/_shared/node-config.js, synced by build-plugin.mjs) and in scripts/lib/repo-fs.mjs
    for the CLIs. It does lstat first. A regular file proceeds. A symlink proceeds only when its
    realpath.native target lies inside the project root and is a regular file. A FIFO, device, socket,
    directory, or a link that escapes the project or dangles is skipped before open. The open uses
    O_NONBLOCK where the platform has it, the fd is re-checked (regular file, size), and a file over
    its bound is skipped, never truncated: MAX_CONFIG_BYTES = 1 MiB for configs, MAX_DOC_BYTES =
    4 MiB for governance docs (measured: the largest real config is 9,114 B and the largest real doc is
    216,465 B). The conductor's two rule-tree walks became one bounded walk, forEachRuleDoc, capped at
    MAX_RULE_WALK_ENTRIES = 5000 entries and MAX_RULE_WALK_DEPTH = 16 levels; an escaping .claude/rules
    root is not entered. The stop hook's language probe reads a 4096-byte prefix through the same helper.
    Your own home files (the global config, the mode switch, the update stamp) keep their symlinks, since
    dotfile managers link them, but still must be a regular file within the bound. Behaviour that is
    now narrower, on purpose:
    a project config or rule file that is a link out of the project, or larger
    than its bound, is ignored by the hooks. verify.mjs <target> reports such an installed file as
    REFUSED (distinct from MISSING) and such a SKILL.md as unreadable. The PowerShell fallback hooks carry the same check as Test-CoalmineSafeFile
    (hooks/_shared/ps-config.ps1), stricter than Node by design: PowerShell 5.1 has no
    realpath.native, so it refuses every reparse point on the file or on any directory between the file
    and the project root, even one that stays inside — test: scripts/lib/repo-fs.test.mjs, the
    CWK-137: tests in scripts/lib/hooks.test.mjs and scripts/lib/ps-config.test.ps1.
  • CWK-137 (2 of 3) — install.mjs wrote through a planted link. With .github/copilot-instructions.md
    linked to ~/.bashrc, install.mjs copilot appended CoalMine's rules block to the shell rc and
    reported success (measured on the newest 1.0.0 tree, v2.0.0 and v3.20.1). The same write-through applied to the platform rules
    file each target writes, an existing git hook or its .pre-coalmine backup slot, the default project
    config, the manifest, and a link on any of the nine project-level agent folders install.mjs writes
    into as of v3.20.1 (.github, .agents, .claude, .gemini, .cursor, .windsurf, .junie, .kiro,
    .augment) that carried the skills install and the default config outside the project. Writes now go through writeRepoFile: the nearest existing ancestor must resolve
    inside the project, the target must not be a link and must be a regular file, and the bytes go to a
    sibling temp opened with wx and are renamed into place, so a link planted after the check is replaced
    and never written through. A refusal is loud: [refused] <path>: <reason>, exit 1, nothing written.
    A hooks directory outside the worktree (a linked worktree's gitdir, an absolute core.hooksPath) is its
    own root, on the reasoning that git configuration chose it rather than a file the repo planted. That holds
    for a git clone, which carries neither .git/config nor a .git file, and not for a tree delivered as an
    archive, so it is a named residual below. Where Windows refuses the rename over
    a file another process holds open (EPERM/EBUSY/EACCES, e.g. re-installing the hooks from inside a
    running pre-commit), the write falls back to an in-place write only for a single-link regular file — test:
    scripts/lib/repo-fs.test.mjs and the CWK-137: tests in scripts/lib/install.test.mjs.
  • CWK-137 (3 of 3) — configure.mjs read, backed up and overwrote through a planted link. With the
    project config linked to ~/.bashrc, configure.mjs treated it as malformed, copied its bytes into a
    .bak inside the repository, then overwrote the link target (measured on v3.3.0 and v3.20.1: the rc file
    ended as { "language": "en" } and the .bak held the original bytes). It now checks the read path and
    the write path with checkRepoWriteTarget before any read or backup, refuses with the path named
    (exit 1), reads through readRepoFileBounded, and writes the config and the .bak through
    writeRepoFile. configure.mjs --global keeps its follow-through write to ~/.claude/.coalmine.json,
    because dotfile managers link that file; its read is bounded and regular-file only — test:
    scripts/lib/repo-fs.test.mjs and the CWK-137: tests in scripts/lib/configure.test.mjs.

Residuals, named: a regular file swapped in between the lstat and the open may lie outside the root
(the fd re-check still holds the read to a bounded regular file); a link planted between a write's check and
its rename is replaced, not followed, except on the single-link in-place fallback above, where a link swapped
in between its lstat and its open is not caught. A tree delivered as an archive can carry a planted
.git/config with an outside core.hooksPath (or a .git file naming an outside gitdir), and install.mjs
will then replace git hooks in that directory; the bound is that the bytes are CoalMine's own fixed gate
script, never attacker text, and an existing hook is first kept as <hook>.pre-coalmine. An agent's own
file reads through its tools are the host's permission system, not covered here.

What you need to do: update. On Claude Code run claude plugin update coalmine@coalmine; users of coalmine@claude-community receive it when that catalog's pin moves; for any other agent, update your CoalMine checkout and re-run node scripts/install.mjs <agent>. If you ran CoalMine in a clone you did not write, the "What to check" list in the advisory says what to look for.

v3.20.1 - 25 CodeRabbit claims adjudicated, the pre-transfer repository address swept from the dist, and the SkillSpector pin refreshed

Choose a tag to compare

@github-actions github-actions released this 22 Sep 08:10
v3.20.1
59ee1e7

Changed

  • CWK-121 (b) — the shipped .claude-plugin/plugin.json homepage/repository fields,
    coalmine-conductor.js's self-error-report URL, commands/update.md's latest-tag check, and the
    shared escalation-footer.md's self-error-report URL (renders into all 9 canary SKILL.md bodies)
    still pointed at the pre-transfer github.com/HetCreep/CoalMine address (redirects, but the dist
    should carry the live one). All now read github.com/TheColliery/CoalMine. plugin.json's
    author.name ("HetCreep") is left unchanged — it names the person, not the repo address.

Fixed

  • CWK-120 row 10 — commands/update.md's latest-tag check could select an annotated tag's peeled
    ^{} deref record instead of the release tag itself
    , since git ls-remote --tags | head -1 has
    no guarantee the plain and ^{} lines for one tag sort adjacently. Filtered with
    awk '!/\^\{\}$/ { print; exit }' before selecting — test: none (a shell-pipeline correctness fix,
    no test harness covers commands/*.md prose).
  • CWK-120 row 5 — rot-canary/SKILL.md's Fix-mode menu condition was self-contradictory: it opened
    "in an interactive session" and then separately listed "no user is present" as a skip clause inside
    that already-interactive scope. Reworded to one non-overlapping condition, matching the shared
    escalation-footer.md Hook Context rule's own vocabulary — no behavior change, the menu still offers
    on any interactive session (manual or hook-nudged) and stays report-only when non-interactive — test:
    none (a legibility fix, no test harness reads SKILL.md prose for this condition).
  • CWK-120 row 6 — scale-canary/SKILL.md's Fix-mode checkpoint instructed git stash/git commit
    as an ad-hoc backup
    , which can hide (stash) or include (commit) unrelated staged/unstaged user
    work sitting in the same repo. Changed to: back up only the touched file(s), or use an isolated
    worktree — never git stash/git commit — test: none (a prose safety-instruction fix).
  • CWK-120 row 7 — telemetry-canary/SKILL.md and testability-canary/SKILL.md's Fix-mode
    "auto-revert if newly red" had no baseline to compare against
    , so the agent could not tell a
    post-edit failure was NEW versus already-failing before the edit. Both now record a build+test
    baseline before applying, and auto-revert only on a failure that is new versus that baseline — test:
    none (a prose safety-instruction fix).
  • CWK-120 row 23 — rot-canary/references/tooling.md's Python row listed bare python -W error
    as a project check
    , which runs no project code, tests, or static analysis (it starts the REPL in
    an interactive shell). Replaced with the project's own test command + -W error (e.g.
    python -m pytest -W error) — test: none (a reference-table content fix).
  • CWK-120 row 14 — the Antigravity auto-cadence status in rot-canary/references/cadence.md
    (shipped) cited only the 2026-07-12 pilot's live fire, omitting the 2026-08-04 isolated re-test that
    recorded ZERO fires on a real AG 2.0 install.
    platform-configs/hooks/antigravity-hooks.json's own
    $comment already discloses both measurements; the shipped cadence reference and the root README's
    primed definition and platform-configs/hooks/README.md's AG row (neither ships into plugin/)
    now say the same: firing is UNRESOLVED, not verified, and a reader is told to probe their own copy
    before relying on it — test: none (an accuracy fix; no fabricated resolution of the contested fact).
  • CWK-120 rows 22/24 — four coalmine: verified reference stamps re-verified content-first, not
    merely re-dated
    (drift-canary/references/checks.md, gold-standard/references/method.md,
    telemetry-canary/references/checks.md, supply-chain-audit/references/tooling.md, all revalidate 90d, all overdue since 2026-09-10): each file's content was re-read in full and confirmed still
    accurate before its stamp moved to 2026-09-22. skills/_shared/references/escalation.md (revalidate 30d, overdue since 2026-08-22) is left EXPIRED and undisposed here — its per-platform Heavy-tier
    levers (Cursor Max Mode, Amp Oracle, GitHub Copilot /fleet, …) are exactly the fast-moving
    version-sensitive claims this room's own doctrine says need a live source-grounding pass, not a
    same-unit rubber-stamp; the file's own text already tells a reader to verify live rather than trust
    it. Routed upward as a pending decision — test: none (stamp-and-content maintenance).
  • CWK-120 SAME-BATCH CLASS SWEEP — the two overclaims rows 2/15 and row 6 fixed on one surface each
    stood uncorrected on their siblings, against this room's own MUST-class ONE FLOCK ONE COLOR rule
    (AGENTS.md, consequence (1): a fix is swept to every sibling surface IN THE SAME BATCH).

    Re-derived both surface sets fresh by grep rather than trusting the prior unit's own count (which
    undercounted the second class by one). The config-cascade "project wins per key" overclaim
    checked against hooks/_shared/node-config.js's real clamp code, not restated by feel — corrected
    on platform-configs/copilot-instructions.template, platform-configs/cursor.mdc.template,
    commands/stats.md, commands/update.md, and the shared skills/_shared/language-header.md
    (renders into all 9 canary SKILL.md bodies — the highest-blast-radius instance of this class).
    skills/rot-canary/SKILL.md:38's own "project wins per key" is DIFFERENT and left alone: it
    scopes to autoFixMode alone, which is genuinely unclamped (not one of the 6 SAFER_ENUM/
    UNION_ARRAY_KEYS keys) — the claim is true as written for that one key, not the same overclaim.
    The git stash/git commit checkpoint data-integrity hazard — corrected on
    drift-canary/SKILL.md, rot-canary/SKILL.md, telemetry-canary/SKILL.md, and
    testability-canary/SKILL.md (4 siblings, one more than the prior unit's own estimate of 3 —
    drift-canary was the uncounted instance). Same correction text as the exemplar fix in both
    classes, no rewording en route — test: none (prose safety/precedence-instruction fixes, no test
    harness reads SKILL.md/command prose for this content).
  • CWK-120 FINDINGS-BACK — the class sweep above shipped a clamp correction that was ITSELF wrong,
    in the PERMISSIVE direction, on all 18 surfaces it touched.
    The new text read "…can only quieten,
    never escalate, an explicit global…", implying the safety clamp does not bind when the global layer
    is unset. Measured against hooks/_shared/node-config.js:290
    (const globalValue = globalVal !== undefined ? globalVal : def;): an ABSENT global reads as the
    SCHEMA DEFAULT and the clamp still binds — with no global config at all, a project's
    scanEverything: true still resolves to false. README.md:189 already states this correctly; the
    swept text disagreed with this repo's own README. Corrected on the same 7 source files (7 →
    18 with their plugin/ mirrors and shared-partial renders): "(a project can only quieten, never
    escalate; an absent global reads as the schema default and is clamped the same way)" — derived from
    README.md:189 and the clamp code directly, not a third composed wording — test: none (prose
    correctness fix; hooks/_shared/node-config.js itself is untouched and its own test suite covers
    the clamp behavior this text now accurately describes).
  • CWK-120 FINDINGS-BACK — row 7's build+test-baseline fix had a third, unswept sibling set, and two
    files now contradicted THEMSELVES.
    telemetry-canary/SKILL.md and testability-canary/SKILL.md
    had their Fix-mode bullet (:26) updated to require a baseline while their own grants table
    (:34, eight lines below) still read "auto-revert if newly red" — an agent reading the second half
    of the file got back the exact defect row 7 removed from the first half. Closed together with the
    unswept class: drift-canary/SKILL.md, rot-canary/SKILL.md (both its Fix-mode bullet and its
    standing-consent line), and scale-canary/SKILL.md all gained the same baseline-before-revert
    language, and all five files' grants tables now read "checkpoint → baseline → build+tests →
    auto-revert only on a NEW failure" — test: none (prose safety-instruction fix, matching row 7's own
    test disposition).
  • CWK-120 FINDINGS-BACK ROUND 2 — the round-1 baseline-class sweep (row 7 + its findings-back close)
    had a THIRD unswept form, a comma/space spelling neither grep pass matched: gold-standard/SKILL.md
    and resilience-audit/SKILL.md (2 sites) still read "checkpoint → [fix] → build+tests → revert if
    newly red" with no baseline concept anywhere in either file.
    Closed the same way as the rest of the
    class: both now record a build+test BASELINE before applying and revert only on a failure new versus
    it, in both the Fix-mode bullet and (for resilience-audit) its grants-table row. A form-independent
    sweep (grep -rln "revert\|rollback\|undo" skills/*/SKILL.md) confirms exactly these 7 files carry
    the class now (drift-canary, gold-standard, resilience-audit, rot-canary, scale-canary,
    telemetry-canary, testability-canary) and no eighth shape — supply-chain-audit is correctly
    outside the class (checkpoint → apply → verify, no build/test revert step at all) — test: none
    (prose safety-instruction fix, matching the rest of the class's own disposition).
  • CWK-120 FINDINGS-BACK ROUND 2 — the permissive-clamp correction (round 1's findings-back) read as
    EXHAUSTIVE, and a real, pre-existing, code-side gap sits behind that reading.
    node-config.js:293
    (if (gi === -1 || pi === -1) continue;) lets an unrecognized project value escape the clamp
    entirely and win the plain merge — a value outside the enum, not merely a louder one inside it. The
    ship-text fix is TEXT-ONLY, per the reviewer's own explicit bound (the clamp's behavior is a shipped
    safety guard and is not changed here, unproven, at the...
Read more

v3.20.0 - both legacy project-config paths honoured and deprecated; a config at a non-candidate path is reported

Choose a tag to compare

@github-actions github-actions released this 21 Sep 11:48
v3.20.0
2b15c04

Added

  • A project config written where the walk does not read it is now REPORTED, not silently ignored (UMB-133). The session-start
    conductor checks a fixed, closed list of plausible wrong homes -- <project>/.agents/.coalmine.json,
    <project>/.gemini/.coalmine.json, <project>/coal/coalmine.json (agent dir dropped) and
    <project>/.claude/coalmine.json (coal/ dropped) -- and, if one exists, adds one context line
    IGNORED: <path> is not a config path; canonical = .claude/coal/coalmine.json for the agent to relay (settings in
    that file have NO effect). It rides the channel each mode already has (Claude Code session context, Antigravity
    injectSteps, Gemini additionalContext) -- no new channel, nothing on stderr (Phoenix #13). The probe is
    existsSync on those fixed paths: no directory walk, and the text around each path is constant, so a cloned repo
    cannot steer what the line says. HONEST BOUND: a config anywhere outside that list is not reported -- this is
    a report of the likely typos, not a search of the project.
    • Cost, stated: a project still on a legacy path (see Deprecated) now carries one extra context line per session
      start until it migrates (roughly 40 tokens).

Deprecated

  • Both legacy per-project config paths -- <project>/.claude/.coalmine.json and <project>/.coalmine.json (UMB-133).
    • Marker + replacement: both are marked DEPRECATED in the README's Configure section, which names the canonical path
      <project>/.claude/coal/coalmine.json verbatim as the replacement. A canonical file always wins over both.
    • Window: deprecated at this MINOR, removable no earlier than the next MAJOR -- this series' own SemVer boundary
      (scripts-quality.md §3), not a calendar count. Until then both keep being read, exactly as before.
    • Owner: CoalMine. node scripts/configure.mjs moves either legacy file to the canonical path on its next write
      (nothing is moved on a mere read).
    • Channel: this entry and the README note. No hook prints a deprecation warning -- Phoenix #13 keeps hooks
      silent on stdout/stderr, so nothing appears in the terminal. The one runtime signal is the conductor's single
      migration-notice context line described under Added, sent only when the config actually read is a legacy one.

Fixed

  • A project config at <project>/.claude/.coalmine.json was silently ignored (UMB-133). It was never a candidate in the
    per-project read order -- only the root dotfile was honoured as legacy -- so a config written where a user reasonably
    expects it had no effect and nothing said so. The order is now canonical (own agent dir, then .agents, then .gemini),
    then <project>/.claude/.coalmine.json, then <project>/.coalmine.json; first found wins, and the merge, the safer-value
    clamp and the global layer are untouched. configure.mjs and install.mjs honour both shapes too (a writer blind to the
    nested one would have written a fresh canonical file that shadowed it and silently dropped every setting in it).
    • A guard the change needed: when the project root IS the home directory, <root>/.claude/.coalmine.json is the
      GLOBAL config. It is compared by identity (both sides through realpathSync.native) and never treated as a project
      config, so the walk does not anchor at ~ and configure.mjs does not migrate -- move and delete -- the file the
      hooks read as the global layer.
    • The cascade wording on eight agent-instruction surfaces named only ONE legacy shape -- the shared language header
      rendered into all nine skills, rot-canary's fix-mode rail, /coalmine:stats, /coalmine:update and the four
      platform-configs/*.template files -- so an agent following it would have skipped a config the hook reads. All eight now
      name both shapes, in order.
    • PowerShell fallback: not ported, and the gap is named (alt/powershell/README.md): the twins still read only
      <gitroot>/.coalmine.json, so the second legacy shape is one more config they do not see.
  • On the Antigravity and Gemini adapters the conductor read the project config from the hook process's own working
    directory, not from the workspace it was reporting on (UMB-133).
    The workspace is named by the hook's stdin payload
    (Antigravity: workspacePaths[0], falling back to the payload's cwd; Gemini: the payload's cwd). The conductor now
    reads that workspace's project config, and the config gates follow it -- enableConductor, disabledCanaries,
    updateMode -- not only the new migration / IGNORED lines. Before, the gates and the config both came from wherever
    the hook process happened to start, while the lines beside them were computed for the workspace, so a workspace's
    own config had no effect when the two differed. User-visible: on those two adapters, a workspace whose config sets
    enableConductor: false, a disabledCanaries list or an updateMode now takes effect where it silently did not;
    and a config sitting only in the hook process's start directory no longer governs a different workspace.
    • Unchanged: Claude Code and the file-copy platforms (they read from the process directory as before), a payload that
      names no workspace (falls back to the process directory), and rot-canary's own hooks (they still read from the
      process directory).
    • The safer-value clamp is unaffected: it runs inside the config merge for whatever directory is read, so a
      workspace config can quieten updateMode but never escalate it past the global layer.
  • A Coal uninstall could delete a repo's own TRACKED hook files (CWK-096).* uninstallGitHooks()
    resolves core.hooksPath (correct since d1c917f) but then unlinked whatever it found there with
    no tracked-ness check -- in any repo whose core.hooksPath points at a versioned directory (this
    room's own .githooks/ included), that deleted the repo maintainer's tracked hooks. Tracked-ness is
    now asked of git (git ls-files --error-unmatch), never inferred: a confirmed tracked or an
    unknown (could-not-tell) answer REFUSES loudly and exits non-zero, naming the file and why; only a
    confirmed untracked answer deletes. The question is asked only when the resolved hooks dir sits
    INSIDE the worktree -- the ordinary <gitDir>/hooks case is untracked by construction and is
    unaffected. Distributed via the Universal Installer (scripts/install.mjs), a surface outside
    plugin/ (Option B) -- same shipped-surface-outside-the-dist precedent as d1c917f.

v3.19.0 - ships the dist that had been sitting untagged, and closes a shell injection in the changed-paths classifier

Choose a tag to compare

@github-actions github-actions released this 09 Sep 20:32
v3.19.0
0d2460a

Ships the dist that had been sitting untagged: 42 commits past v3.18.3 moved 14 files under plugin/ (all three hooks plus every skill body) while plugin.json still read 3.18.3 — and the publish event is the version-string bump on the default branch, so none of it reached claude plugin update until now.

MINOR, not patch: the release block carries two ### Added sections (per-workflow concurrency groups; the fully-suppressed-auto-scan notice with its scanEverything status), which is MINOR-minimum under this flock's own sizing rule.

New in this commit (comment only, no behaviour change) — CWK-087/CWK-088. The Stop hook's emit block now records the measured cost of its own loud channel. Measured on Claude Code 2.1.266 with a discriminating pair, one turn each:

Stop hook emits result num_turns
decision:"block" + reason the hook's text — the session's own answer is gone 2
systemMessage only the session's answer, intact 1

The block is not a defect to silence — it is what makes the agent actually run the scan — and the hook has no honest way to detect a -p session: the Stop payload carries no mode field, hook stdio is never a TTY (the platform pipes the payload in and reads stdout back), and CLAUDE_CODE_ENTRYPOINT is inherited, so a -p child of an interactive session reads that parent's value. The consequence therefore belongs to the caller: read a child's report from disk, or run --output-format stream-json --verbose (verbose is required — the CLI refuses otherwise), which keeps the pre-hook answer as its own assistant event, delivers the hook's reason as a user event, and marks the blocked turn system/post_turn_summary with status_category:"blocked".

Everything else in this release shipped in earlier commits; the full list is in CHANGELOG.md under [3.19.0].

Gates: test.mjs 294 pass / 0 fail / 5 skipped · verify.mjs PASS · consistency.mjs PASS · release-gate.ps1 CoalMine PASS.

v3.18.3 - explicit mode on every shipped os.tmpdir() write

Choose a tag to compare

@github-actions github-actions released this 31 Aug 14:24
v3.18.3
0993976

Every os.tmpdir() write in the shipped hooks now passes an explicit restrictive mode.

Security

  • All six os.tmpdir() writes now pass mode: 0o600 (CodeQL js/insecure-temporary-file #66/#67). The alerts fired on v3.18.2's own sweep-marker hardening. Adjudicated by reading the query's own definition rather than assuming a false positive: it fires on a missing mode argument, or a mode whose lowest 6 bits are nonzero, and reads neither the wx flag, nor the enclosing 0o700 directory, nor the lstat guards, nor filename randomness — so a random filename suffix would have closed nothing. The finding is real if low-impact: the stamp was created at default mode inside a directory whose own 0o700 is a no-op when it already exists, which is the residual the v3.18.2 round had recorded and accepted. Two appendFileSync writes carrying the user's edited file paths were hardened too, although they sit outside the query's sink list — letting a scanner's sink list draw the threat boundary would be the tail wagging the dog. flag: 'wx' is preserved at every site that had it.

Notes

  • The mode assertion skips on Windows and exercises on the Linux and macOS CI legs: NTFS reports the same bits either way, so the check cannot discriminate there. The skip is visible and named, never a silent pass.

Back-filled 2026-08-31: body written from the CHANGELOG [3.18.3] entry's own headings.

v3.18.2 - sweep-marker symlink hardening

Choose a tag to compare

@github-actions github-actions released this 31 Aug 05:39
v3.18.2
1b614e4

Hardens the rot-canary sweep marker so a planted symlink can no longer redirect it.

Security

rot-canary Stop hook: the tmp-sweep throttle marker no longer writes through a planted symlink (CWK-031/U8, CoalBoard nasa-L3 round). The 24h throttle marker sat flat in the shared tmp root under a fixed name, written with the default w flag, which follows a symlink at the destination. On Unix the shared /tmp let any local user pre-plant a link at that name; the victim next Stop truncated the link target (truncate-to-empty / empty-file creation, not arbitrary content).

Fixed by moving the marker into the private os.tmpdir()/coalmine/ subdir (mode 0700 + the lstat dir guard) and re-stamping via a per-pid temp + rename rather than a plain write. A symlink at the marker path is never obeyed as a gate, so it cannot suppress the sweep either.

Full detail: CHANGELOG.md [3.18.2].

Full Changelog: v3.18.1...v3.18.2

v3.18.1 - SKILL.md body-lean campaign pass

Choose a tag to compare

@github-actions github-actions released this 22 Aug 17:19
v3.18.1
f990e84

Compression pass over the skill bodies: one real cut, eight already lean.

Fixed

SKILL.md body-lean campaign (campaign #6 unit 6, belt 2/board 2). Measured via before touching anything; 8 of 9 skills declared already-lean (no fat found, no forced cuts); gold-standard (the one skill over the ~5000-tok body budget) had a Discipline section restating Prohibitions P10/P11/P12 and Degrade-path D1 near-verbatim -- collapsed to two lines pointing at the ledgers, matching this room's own established Dedup convention.

Full 3-tier variance walk (weak/medium/strong, 11 leaves) on the changed prose: zero variance on every enumerated rail.

Full detail: CHANGELOG.md [3.18.1].

Full Changelog: v3.18.0...v3.18.1

v3.18.0 - CLASSIFY-BLOCK grants sections on 8 skills

Choose a tag to compare

@github-actions github-actions released this 21 Aug 10:14
v3.18.0
6044d78

Added

Retrofits (CLASSIFY-BLOCK, board #93) onto 8 of CoalMine's 9 skills: a declared "Grants & denials" table stating each skill's read/write(/network) grants and what a permission denial does (report + never claim done, never a false clean bill) -- source-grounding deliberately excluded (read+network only, its own D1 already covers the one hot class).

New shared partial skills/_shared/classify-block.md injected via a new <!-- SHARED:CLASSIFY_BLOCK --> marker in render.mjs; verify.mjs now enforces both content-match AND presence for the marker across all 9 skills.

Full findings-back + RE-INSPECT trail (INSPECT: 3 MEDIUM + 2 LOW, all closed, SHIP verdict) in CHANGELOG.md.

Full Changelog: v3.17.4...v3.18.0

Fixed

  • scripts/lib/claude-ai-trim.mjs's trimDescription() could split a non-BMP character mid-surrogate-pair (board #40 fixback F3), leaving a lone high surrogate that decodes to U+FFFD. Nothing is live today -- every shipped description is English with ASCII spaces -- but this org writes Thai, so it is a when-not-if fix. Fixed by dropping the one trailing lone high surrogate before the whitespace rescue, chosen over a code-point rewrite that would change what "200 chars" means. Does not reach plugin/; it does change what a user downloads from the Release. New regression test, confirmed red before the fix.

Back-filled 2026-08-31: body re-shaped to the CHANGELOG [3.18.0] entry's own headings; written from that entry.

v3.17.4 - claude-ai-trim surrogate-pair fix

Choose a tag to compare

@github-actions github-actions released this 16 Aug 09:29
v3.17.4
c5f56cf

Least-privilege and workflow-guard fixes, plus three CodeQL alerts dismissed with corrected reasoning.

Fixed

  • claude-ai-trim.mjs surrogate-pair fix (board #40 fixback F3) -- a non-BMP character at the trim boundary could split mid-surrogate-pair with no ASCII word break to rescue it.
  • Least-privilege token on claude-ai-zips.yml (board #117/#118): contents: write moved off the top level onto the one job that needs it; a workflow_dispatch-on-main hole closed (a re-run against the default branch could no longer attempt a Release tagged "main").
  • create-or-update release-publishing tooling (scripts/lib/publish-release.mjs) plus its own 422-already-exists race-window close.

See CHANGELOG.md for full detail.

v3.17.3 - three more hook config gates clamped

Choose a tag to compare

@HetCreep HetCreep released this 13 Aug 13:55
v3.17.3
0c0fa72

Closes board #112's own follow-up: three more hook-read config gates now honor the same safer-value-wins clamp as updateMode, in both Node and PowerShell.

Security

  • enableConductor, disabledCanaries, and rotCanaryMode (+ their legacy aliases conductor/disable/mode) join the merge's safer-value-wins clamp — closing board #112's own next-touch set (board #113). Before this, a project-only .coalmine.json could silently re-enable a globally-disabled canary, flip the conductor back on, or re-arm auto-scanning past an explicit global choice.
  • disabledCanaries clamps by UNION: the safer array direction is union, never pick-one-side), not by ordered enum.
  • Two escalation holes found auditing the fix, both closed: a legacy key name (e.g. disable for disabledCanaries) could bypass the new-key clamp entirely; and the merge was storing the raw-cased winning value instead of the canonical enum member, which could silently fail a strict === comparison downstream.
  • Ported to the PowerShell twin (ps-config.ps1), all three keys plus legacy aliases.
  • Findings-back: main's CI had been red since v3.17.0 — the gate-wired PowerShell test suite was still asserting board #112's pre-fix behavior, and nobody caught it locally because pwsh is absent on the dev box (the pre-commit gate silently skips it there). Fixed.

Back-filled 2026-08-31: body re-shaped to the CHANGELOG [3.17.3] entry's own headings; written from that entry.