fix: close the v0.22.1 re-audit medium-severity findings (ME-03..ME-22)#88
Merged
Conversation
…ction; catch nested repair errors (ME-15..ME-18)
Replace existence-only probes with functional ones so doctor can no longer
report a broken subsystem as healthy. All checks stay fail-open and keep the
ok/warn/fail/na result shape.
- ME-15 settings: verify the ACTUAL Forge wiring — every REQUIRED guard identity
from the settings template (compared via the exported guardKey) plus permissions
— instead of trusting the `_forge` marker + any hook. A marker with one unrelated
hook is now DEGRADED, ACTIVE only when the expected guard set is installed.
- ME-16 install: `~/.forge` must be a symlink/dir whose required guard assets
(protect-paths.sh, secret-redact.sh/.mjs) resolve, are readable, and (for .sh)
executable. A plain file, a dangling symlink, or a partial install is now
UNAVAILABLE/FAILED; ACTIVE only when the assets resolve.
- ME-17 redaction: run a safe, side-effect-free self-test — a runtime-assembled
fake secret fed through the real secret-redact.mjs — and require it to redact
and preserve output shape. ACTIVE only on a passing round-trip; DEGRADED when
node/redactor is missing or the output is wrong. Health maps secret-redaction
to this self-test, not the generic node check.
- ME-18 repairs: repairFailure inspects NESTED report rows — a partial sync whose
top-level result is fine but whose emitter rows carry action:"error" (or a
writeForgeConfig-style {ok:false}) is now recorded ok:false with the reason.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
…ata; trusted quarantine identity (ME-05, ME-06, ME-07) ME-05: separate record-integrity validity from evidence RESOLUTION. validateRef now carries real format grammars — `ci:` must be a CI locator (URL / owner/repo@run / run id), `human:` must be an explicit ratification (author@ref), and `file:` resolves to an existing path when a repo root is available (pure/no-throw, mirroring the `git:` resolver). A new pure refStrength() classifies each ref as RESOLVED (untyped/legacy, git, ci, human) or FORMAT-ONLY (file, test). In val(), format-only evidence counts at UNRESOLVED_WEIGHT and a claim with no resolved confirmation is capped at UNRESOLVED_VAL_CAP (0.55) — below the reuse SERVE_FLOOR (0.6) and the trusted band (0.65). So `test:made-up-run` / `file:/does/not/exist` can never lift confidence into the serving range, while a resolvable git-ref confirmation raises it exactly as before. Strength is re-derived purely from the ref, never trusted from the stored record (same discipline as the ORACLES weights). ME-06: run the secret detector over evidence refs/authors, tombstone reasons, and provenance metadata BEFORE append. outcomeRecord refuses secret-shaped ref/author at mint; appendRecord is the single persist choke point that refuses any secret-bearing metadata record, so no channel (evidence, provenance, tombstone, import) can write a credential to disk. ME-07: quarantine dedups by a TRUSTED contentHash over the (redacted) rejected record plus its rejection reason — never the record's attacker-chosen `h`. Two distinct forged records sharing one fake `h` now get distinct identities and both survive; malformed no-`h` lines are captured instead of silently dropped; and the stored record is redacted so quarantine never persists raw secret material. mergeDirs now scans the source logs RAW line-by-line (instead of through the hash-deduping read path) so nothing is collapsed or lost before quarantine. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
…; explicit PARTIAL sync status (ME-03, ME-19, ME-20)
ME-03: detectStack() read manifests only at the repo root, so npm/pnpm/yarn
workspaces and Turborepo/lerna/Maven/Gradle subprojects (and nested Python
packages) were invisible — a single root test command silently "covered" a
whole monorepo. detectStack now surfaces two additive fields: `workspaces`
(declared globs from package.json workspaces, pnpm-workspace.yaml, lerna.json)
and `packageRoots` (nested package/suite roots found on disk via a bounded,
budgeted, depth-capped BFS — never a deep walk). Both are [] for a plain
single-root repo, so testCommands/testRunners and the rest of the shape are
back-compat.
ME-20: legacy `.forge/rules.json` was parsed with a bare JSON.parse, so a
single typo threw and ABORTED `forge sync`. It now reads through a guarded
parser (like loadConfig): invalid JSON warns once to stderr, falls back to the
default rules, surfaces a sync warning row, and never rewrites the file — the
user's bytes are preserved to fix.
ME-19: sync writes AGENTS.md, then per-tool files, then MCP files and is not
transactional; a mid-way failure was only reported per-target, so init/CLI
could still imply every tool is ready. sync() now returns an explicit
aggregate: `partial` / `status` ("PARTIAL" when any target row is
action:"error", else "OK"). The `forge sync` and `forge init` summaries reflect
PARTIAL and exit non-zero instead of unconditional success. (Chosen the
explicit-status approach over full temp+rename transactionality across every
target: the emit helpers already writeIfChanged, and per-target atomicity would
be a large, invasive change for little gain over an honest aggregate flag.)
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
…ies; atomic add/remove; validate names (ME-08..ME-11)
MCP ownership was global-by-name, so `--adopt context7` could authorise
overwriting every target's same-name entry, built-in registry names were
implicitly owned (letting a divergent user `forge-cortex` be clobbered),
add/remove left disk and the managed-set record inconsistent on partial
failure, and the exported emitter trusted arbitrary future server names.
- ME-08: record adoption PER TARGET as `{server, target}` pairs in
`mcp.adopted`; `owns(target, name)` consults only that target. Legacy bare
names are still honoured as a wildcard and migrated to pairs on next write.
- ME-09: registry names carry no implicit ownership — a divergent same-name
entry is preserved and reported with the adopt hint; forge refreshes only
what it can prove it wrote (byte-identical spec, forge marker, or adoption).
- ME-10: add emits all targets first and records only on full success; remove
cleans all targets first and drops the record only when every cleanup
succeeded — per-target write/cleanup failures surface as `error` rows and
are reported as an incomplete transaction instead of throwing.
- ME-11: validate every managed name against `^[a-z0-9][a-z0-9_-]{0,63}$`
before any write, reject `foo`/`forge-foo` Continue-filename collisions, and
serialise command strings safely into YAML (quoted) and TOML.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
…tes; complete tool list; exact gitattributes rule; disclose before merge (ME-12,13,14,21,22)
ME-12: readConfigFile (repo_config) and readExistingSettings (init) now treat
valid-but-non-object top-level JSON (null / [] / "x" / 42) as CORRUPT, not as
empty {}. Reads report corrupt and writes refuse, so a later write can never
silently replace bytes a human may still want to fix.
ME-13: writeForgeConfig now backs up an existing valid config (timestamped) and
writes via a temp file + atomic rename — a crash mid-write can no longer truncate
the unified config. The corrupt-refusal path (ME-12) is preserved.
ME-14: reconciled KNOWN_TOOLS and primary-tool auto-detection with the emit
targets — added roo to KNOWN_TOOLS and added on-disk detection markers for
aider (.aider.conf.yml), continue (.continue), windsurf (.windsurf), and roo
(.roo). `forge tools <name>` now accepts all of them; docs updated.
ME-21: ensureLedgerGitattributes now parses the exact active attribute LINE
(non-comment pattern + attr) instead of a substring check, so a comment merely
mentioning .forge/ledger/ no longer suppresses the real union-merge rule; an
existing real rule is still not duplicated.
ME-22: the GLOBAL-settings disclosure is now emitted BEFORE the merge mutates
~/.claude/settings.json — mergeSettings fires an onNotice(target) hook prior to
any write, threaded through init as onSettingsNotice, and the CLI prints the
consent line via that hook instead of after the fact.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
Consolidated [Unreleased] entries for the ledger, MCP, doctor, config/init, and stack/sync workstreams. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
CodeWithJuber
marked this pull request as ready for review
July 19, 2026 12:45
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Completes the v0.22.1 edge-case re-audit remediation, following the merged criticals (#85) and high-severity fixes (#87). This PR closes the medium-severity findings — robustness and trust-boundary polish — as five independently-green workstream commits plus a consolidated changelog. Only ME-23 (hook shell→exec-form migration) is deferred: it would churn the just-landed HI-05 ownership manifest and RA-12 quoting for a "safer design" that isn't a correctness bug.
Ledger (ME-05, ME-06, ME-07) —
src/ledger.js,src/ledger_store.jsci:/human:/file:are format- and existence-checked, and unresolved pointers liketest:<id>or a missingfile:path can't lift confidence into the trusted/serving band (they count at reduced weight, capped below the serve floor). A resolvable git-ref confirmation is unchanged.MCP (ME-08, ME-09, ME-10, ME-11) —
src/emit/mcp.js,src/integrations.js{server, target}(legacyadopted: [name]still honored), so adopting a same-name server for one tool never authorizes overwriting another's. A divergent user entry sharing a built-in name (forge-cortex) is preserved, not overwritten. Add/remove are atomic (record nothing on partial emit; keep the entry and report incomplete if cleanup fails). Names must match^[a-z0-9][a-z0-9_-]{0,63}$,foo/forge-foofilename collisions are rejected, and commands are serialized safely into YAML/TOML.Doctor (ME-15, ME-16, ME-17, ME-18) —
src/doctor.js~/.forgeis a symlink/dir whose guard assets resolve (catching plain-file shadows and dangling links), runs a live redactor self-test, and records a partial--fixsync (nestedaction:"error"rows) as a failed repair.Config / init (ME-12, ME-13, ME-14, ME-21, ME-22) —
src/repo_config.js,src/init.js,src/cli.jsnull/[]/"x"/42) is treated as corrupt (bytes preserved) for both repo config and settings;forge.config.jsonwrites are backed-up + temp-file+rename atomic;roois a selectable tool and aider/continue/windsurf/roo auto-detect; the ledger.gitattributesrule is matched by exact line (a comment no longer suppresses it); the global-settings merge is disclosed before it mutates the file.Stack / sync (ME-03, ME-19, ME-20) —
src/stack.js,src/sync.jsdetectStack()surfacesworkspaces+packageRoots(bounded scan) so a root command can't silently claim to cover a monorepo;forge syncreturns an explicitPARTIALstatus (and exits non-zero) when any target fails; a corrupt legacy.forge/rules.jsonwarns and falls back instead of aborting sync.Integrated branch: 1056 tests / 0 failures, Biome + tsc +
forge docs checkgreen,forge verifyPASS.Checklist
npm testpasses (1056 tests, 0 fail; ~40 new regression tests)npm run checkpasses (Biome)fix:/docs:)CHANGELOG.mdupdated under## [Unreleased]forge toolslist, GUIDE;forge docs checkgreen)Risk & rollback
Extra checks (tick if applicable)
npm run typecheckpassestest/_fixtures.js)🤖 Generated with Claude Code
https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
Generated by Claude Code