diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c29b189..872fa28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,13 +53,13 @@ jobs: - run: bun run lint:skills - run: bun run lint:agents - run: bun run lint:profiles - - name: lint:knowledge (no-op without KNOWLEDGE_REPO_PATH) - # No team-knowledge repo checkout lives in this CI job — the fixture - # question (a real knowledge-note dir to lint here) is deliberately - # left open, see issue #91 / PR body. Without KNOWLEDGE_REPO_PATH set - # this only proves the script runs cleanly (prints a "no directory - # specified" message and exits 0); it does not lint real notes yet. - run: bun run lint:knowledge + - name: lint:knowledge (fixture dir, not the real team-knowledge repo) + # No team-knowledge repo checkout lives in this CI job, so this lints + # tests/fixtures/knowledge/ instead — a small schema-valid note kept + # in-repo so the linting logic (schema checks, name/filename + # mismatch, supersedes-unknown warning) actually runs in CI rather + # than short-circuiting on "no directory specified". + run: bun run lint:knowledge tests/fixtures/knowledge schemas: # Fails if `bun run schemas:emit` would produce a diff — enforces that the diff --git a/MANUAL.md b/MANUAL.md index 301c449..34f7ffd 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -469,7 +469,6 @@ cc-settings ships a **core** set of MCP servers — installed automatically by ` | `tldr` | Semantic codebase analysis (call graphs, impact) | `/tldr`, `/explore` | | `figma` | Figma Dev Mode MCP — design tokens, component props | Auto-triggered by the server's own instructions on figma.com URLs; `/qa` for design-fidelity checks | | `chrome-devtools` | Chrome DevTools (perf traces, network, console, screenshots, a11y tree, click/fill, lighthouse) | `/lighthouse`, `/qa`, `/fix`, `tester` agent, Figma-MCP design-vs-implementation diffs | -| `Sanity` | Sanity CMS operations (GROQ queries, etc.) | satus / novus projects with Sanity integration | **Optional** servers — not installed by default; add manually to `~/.claude.json` if you want them. Listed in `mcp-configs/recommended.json`: @@ -478,6 +477,7 @@ cc-settings ships a **core** set of MCP servers — installed automatically by ` | `github` | GitHub issues / PRs / projects | `gh` CLI covers most of this with lower context cost | | `vercel` | Deployment management | Stack-specific (Vercel-only) | | `memory` | Persistent cross-session memory | cc-settings has its own `~/.claude/memory/` system | +| `Sanity` | Sanity CMS operations (GROQ queries, etc.) | Project-specific (satus / novus with Sanity) — not in `config/20-mcp.json`, so `setup.sh` never installs it | The post-install summary groups MCP servers by status (`core` / `optional` / `user-added`) so a new joiner can tell which came from cc-settings vs which they added themselves. @@ -489,7 +489,7 @@ These are enforced automatically — no skill needed: - **2-iteration limit** — fails twice? Stop, pivot, present alternatives - **Bug fix scope** — only touch files related to the bug -- **Pre-commit verification** — tsc + build + tests must pass before any commit +- **Pre-commit verification** — `tsc --noEmit` is hooked and blocks `git commit` on type errors; build + tests are not hooked — run them yourself via `bun run proof` or the relevant skill before committing - **Post-compaction recovery** — re-read plan + active files after compaction - **Neutral exploration** — agents investigate without bias toward expected outcomes - **No AI attribution** — stealth mode in all commits and PRs @@ -565,7 +565,6 @@ Hook types: `command` (shell), `prompt` (LLM yes/no), `agent` (subagent with too | Event | What Happens | |-------|-------------| | Session start | Load learnings, check TLDR index | -| Setup | Runs on `--init`, `--init-only`, or `--maintenance` CLI flags | | User prompt | Delegation breadth detection (`delegation-detector.ts`), session title | | Pre-tool (Bash) | Safety net, pre-commit TSC, docs check before install | | Pre-tool (Edit) | Stale file detection | diff --git a/README.md b/README.md index aa60bed..0f91eaf 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/dar Both one-liners clone the repo and run the installer. To pass flags (`--light`, `--dry-run`, …), clone first and run the bootstrap from the checkout: `bash setup.sh --light` or `.\setup.ps1 --light`. -Requires [Bun](https://bun.sh) ≥ 1.2.21 and git — the bootstrap installs Bun automatically if missing. Re-installs are non-destructive: existing permissions, custom hooks, and local overrides survive. +Requires [Bun](https://bun.sh) ≥ 1.2.21 and git — the bootstrap installs Bun automatically if missing. The full profile also installs `jq` if missing; no runtime hook shells out to it (all hooks are TypeScript) — it's for the `jq` one-liners used in team-knowledge remediation runbooks. Re-installs are non-destructive: existing permissions, custom hooks, and local overrides survive. Restart Claude Code after install. @@ -57,7 +57,7 @@ No custom CLAUDE.md, agents, rules, profiles, MCP servers, hooks (beyond the sta ├── CLAUDE.md # Claude-Code-specific config ├── settings.json # Composed from config/*.json (permissions, hooks, MCP) ├── agents/ # 10 specialized subagents -├── skills/ # 36 auto-invocable skills +├── skills/ # 37 auto-invocable skills ├── profiles/ # Stack contexts: nextjs, react-native, tauri, webgl, maestro, react-router ├── rules/ # Path-conditioned rules (load on-demand by file type) └── src/ # Hook + script implementations (TypeScript) diff --git a/SECURITY.md b/SECURITY.md index b947ed4..9fe0b9d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -79,6 +79,16 @@ Exit code is non-zero on any suspicious finding so CI can gate on this. Stale-on Source: `src/lib/audit-hooks.ts`, `src/scripts/audit-hooks.ts`. +**Also scanned, in their own report section:** `env` values and `mcpServers` +command/args are matched against the same malware-signature bank +(`curl | sh`, base64 decode + exec, `node -e`, etc.) and printed under a +separate `ENV/MCP SUSPICIOUS` heading when something matches. This is +**pattern-match classification only** — there is no shipped-pattern/manifest +concept for env vars or MCP server definitions, so nothing here is ever +"trusted", and a clean scan is not an integrity guarantee the way a "trusted" +hook finding is. See the scope note below for exactly what this does and +doesn't buy you. + ### 4. The allowlist convention Every hook command that cc-settings ships starts with @@ -194,6 +204,36 @@ Consequences of that stance: - Anything that MUST never run belongs in `config/30-permissions.json`, not (only) in safety-net. +**Keeping the deny-list honest against the allow-list's wholesale +subcommands:** `config/30-permissions.json` wholesale-allows several git +subcommands (`git branch:*`, `git checkout:*`, `git push:*`, …) because +requiring per-flag confirmation on every `git status`/`git log`/routine +`git push` would make the deny-list unusable. That tradeoff only holds if +the deny-list has an explicit override for every destructive *form* of a +wholesale-allowed subcommand — otherwise the dangerous form is silently +auto-approved by the broad allow, with only fail-open safety-net.ts between +it and the user, contradicting the "deny-list is the boundary" claim above. +Concretely: `git push -uf`/`-fu` (bundled short flags that still carry +`-f`, distinct from the literal `--force`/`-f` token forms already denied) +and `git branch -D`/`-d -f` (force branch delete) each have their own deny +entry alongside the existing `--force`/`-f`/`-D` forms, mirroring exactly +what `safety-net.ts`'s `analyzeGitAfterVerb` already treats as must-block. +When adding a new wholesale-allowed git (or other) subcommand, enumerate its +destructive forms against safety-net.ts's checks and close the gap in the +deny-list rather than narrowing the allow — narrowing breaks the common +case (an interactive human confirming a legitimate `git push --force` to +their own fork), closing the gap doesn't. + +**Edit/MultiEdit are wildcard-allowed (`Edit(*)`, `MultiEdit(*)`) alongside +`Write(*)`.** The deny-list's `Write(~/.claude/settings.json)` / +`Write(~/.claude.json)` entries only close the `Write` tool surface for +those two files — `Edit`/`MultiEdit` are separate tool names and are not +covered by a `Write`-scoped deny rule. `Edit(~/.claude/settings.json)`, +`MultiEdit(~/.claude/settings.json)`, and the `.claude.json` equivalents +mirror the `Write` denies for exactly this reason: all three mutation tools +must be denied for a path, not just one of them, or a prompt-injected +"use Edit instead of Write" instruction bypasses the protection entirely. + ## What cc-settings deliberately does not do - **Auto-quarantine on suspicious match.** The session-start hook only @@ -226,6 +266,49 @@ What the content manifest does **not** cover: worms automate against defaults — but a targeted attacker with full user-privilege write access is outside this threat model. +## Scope: the fingerprint and content manifest cover the `hooks` block only + +Layers 1 and 2 above (the hooks-block fingerprint and the src content +manifest) are precisely scoped: `hashHooks()` hashes `settings.hooks` and +nothing else, and the content manifest verifies script *files*, not other +`settings.json` keys. Concretely, **`mcpServers`, `permissions`, `env`, and +every other top-level key of `settings.json` are entirely outside what those +two layers verify** — a change to any of them, however large, never trips +the fingerprint mismatch warning, even though `mcpServers` entries run +arbitrary local commands with full startup (a persistence primitive at +least as strong as a hook). + +Why this boundary, deliberately, rather than widening the hash: `mcpServers`, +`permissions`, and `env` are all things a user is *expected* to hand-edit +routinely (adding a personal MCP server, tightening a permission rule, +setting a project env var) — unlike the `hooks` block, which cc-settings +owns end-to-end and where any drift from the installed state is inherently +suspect. Folding those keys into the fingerprint would mean every legitimate +edit trips the same "tampering" warning as an actual injected payload, +training users to click through it — the opposite of what a security signal +is for. + +What actually covers `mcpServers`/`env`: layer 3's auditor (above) runs a +**best-effort pattern-match scan** (`auditEnvAndMcp` in +`src/lib/audit-hooks.ts`) over `env` values and `mcpServers` command/args +against the same malware-signature bank used for hooks, reported in its own +`ENV/MCP SUSPICIOUS` section. This is classification only — it catches an +*obvious* injected payload (a `curl | sh`, a base64-decode-and-exec, an +inline `node -e`), the same way the hook auditor's `suspicious` tier does. +It does **not** give `mcpServers`/`env` the same guarantee `hooks` gets from +the fingerprint + manifest pair: there's no "this exact value hasn't changed +since install" check, no content-hash verification of anything an +`mcpServers.command` points at, and no `trusted` tier — silence means "no +known signature matched," not "unchanged" or "verified." + +**Manual-review guidance:** if you see an `mcpServers` entry or `env` value +you don't recognize — whether or not the auditor flagged it — treat it the +same as an unrecognized hook: check whether you or a tool you installed +added it, and if not, treat it as a compromise signal and follow the +remediation steps above. `permissions` (the allow/deny lists themselves) has +no scanner at all yet; review changes to it the same way you'd review any +other settings.json diff. + ## Reporting Suspicious activity, false positives, or signature gaps — open an issue at diff --git a/agents/deslopper.md b/agents/deslopper.md index a9b0289..af456aa 100644 --- a/agents/deslopper.md +++ b/agents/deslopper.md @@ -14,7 +14,7 @@ description: | - "before push" / "pre-push review" / "final cleanup" RETURNS: Dead code auto-removed, consolidation recommendations for approval -tools: [Read, Edit, Grep, Glob, LS, Bash, Agent, AskUserQuestion, TeamCreate, SendMessage, TaskCreate, TaskUpdate, TaskList, TaskGet] +tools: [Read, Edit, Grep, Glob, LS, Bash, Agent, AskUserQuestion, TeamCreate, TeamDelete, SendMessage, TaskCreate, TaskUpdate, TaskList, TaskGet] disallowedTools: ["Bash(git push:*)", "Bash(git commit:*)", "Bash(rm:*)"] maxTurns: 40 effort: high diff --git a/agents/maestro.md b/agents/maestro.md index 7110ae1..20a7746 100644 --- a/agents/maestro.md +++ b/agents/maestro.md @@ -11,7 +11,7 @@ description: | - Complex debugging requiring multiple perspectives RETURNS: Orchestration status, synthesized results from sub-agents, progress tracking -tools: [Read, Write, Edit, MultiEdit, Bash, Grep, Glob, LS, TodoWrite, Agent] +tools: [Read, Write, Edit, MultiEdit, Bash, Grep, Glob, LS, TodoWrite, Agent, TeamCreate, TeamDelete, SendMessage, TaskCreate, TaskUpdate, TaskList, TaskGet] disallowedTools: ["Bash(git push:*)", "Bash(rm:*)"] maxTurns: 60 effort: max diff --git a/agents/reviewer.md b/agents/reviewer.md index 68920c0..faef0c0 100644 --- a/agents/reviewer.md +++ b/agents/reviewer.md @@ -17,10 +17,9 @@ disallowedTools: ["Bash(git commit:*)", "Bash(git push:*)", "Bash(rm:*)"] maxTurns: 15 permissionMode: plan effort: high -isolation: worktree color: yellow initialPrompt: | - Run `git diff origin/main...HEAD --stat` then `git diff origin/main...HEAD` (fall back to `git diff HEAD~1` if no upstream) to see the scope of changes before critiquing. Also run `git log --oneline -10` for recent context. Then proceed with the user's review request. + Run `git diff origin/main...HEAD --stat` then `git diff origin/main...HEAD` (fall back to `git diff HEAD~1` if no upstream) to see the scope of changes before critiquing — this runs in the live working tree, so uncommitted and unpushed changes are visible. Also run `git log --oneline -10` for recent context. Then proceed with the user's review request. --- You are an expert code reviewer for Darkroom Engineering projects. diff --git a/agents/security-reviewer.md b/agents/security-reviewer.md index 9f38c16..4cb61fa 100644 --- a/agents/security-reviewer.md +++ b/agents/security-reviewer.md @@ -16,10 +16,9 @@ disallowedTools: ["Bash(git commit:*)", "Bash(git push:*)", "Bash(rm:*)", "Bash( maxTurns: 30 permissionMode: plan effort: high -isolation: worktree color: red initialPrompt: | - Start with automated scans before targeted review: secret detection grep (AKIA|sk-|sk_live|ghp_|shpat_), dangerous-pattern grep (eval|dangerouslySetInnerHTML|innerHTML|exec\(), `bun audit` if a lockfile exists, and `grep NEXT_PUBLIC_.*SECRET` for env leaks. Report the quick-scan baseline first, then proceed with the user's deep-review request. + Start with automated scans before targeted review, over the live working tree (not a fresh worktree, so uncommitted and unpushed changes are in scope): secret detection grep (AKIA|sk-|sk_live|ghp_|shpat_), dangerous-pattern grep (eval|dangerouslySetInnerHTML|innerHTML|exec\(), `bun audit` if a lockfile exists, and `grep NEXT_PUBLIC_.*SECRET` for env leaks. Report the quick-scan baseline first, then proceed with the user's deep-review request. --- You are an expert security reviewer for Darkroom Engineering projects. diff --git a/config/30-permissions.json b/config/30-permissions.json index a62de59..cfb0a1b 100644 --- a/config/30-permissions.json +++ b/config/30-permissions.json @@ -101,6 +101,10 @@ "Write(~/.gnupg/*)", "Bash(git push --force:*)", "Bash(git push -f:*)", + "Bash(git push -uf:*)", + "Bash(git push -fu:*)", + "Bash(git branch -D:*)", + "Bash(git branch -d -f:*)", "Bash(git reset --hard:*)", "Bash(git clean -f:*)", "Bash(git checkout -- .:*)", @@ -133,6 +137,10 @@ "Write(~/.bash_profile)", "Write(~/.claude/settings.json)", "Write(~/.claude.json)", + "Edit(~/.claude/settings.json)", + "MultiEdit(~/.claude/settings.json)", + "Edit(~/.claude.json)", + "MultiEdit(~/.claude.json)", "Bash(gh repo delete:*)", "Bash(gh secret:*)", "Bash(gh api -X DELETE:*)", diff --git a/docs/agent-models.md b/docs/agent-models.md index b7cfe0c..4c5bb22 100644 --- a/docs/agent-models.md +++ b/docs/agent-models.md @@ -16,7 +16,7 @@ Routing principle: **explore and execute on the cheaper tiers, decide on the top |-------|-------|-----------| | `maestro` | **opus[1m]** | Orchestration needs the strongest available reasoning (was `fable`) | | `planner` | **opus[1m]** | Architecture decisions need depth (was `fable`) | -| `oracle` | **opus[1m]** | Expert Q&A needs nuance (was `fable`) | +| `oracle` | *(session model)* (skill, not an agent — `skills/oracle/SKILL.md` runs as a `context: fork` of the main session, no agent binding) | Not a dedicated opus[1m] agent despite the name; the fork inherits the session's model, so on an `opus[1m]` session oracle already thinks at the top tier | | `reviewer` | **sonnet** | Diff-reading is bulk work; cross-model `codex-verifier` provides the independent second gate | | `implementer` | **sonnet** | Executes already-made plans; Sonnet 5 is near-Opus on coding, and plans come from the top tier | | `security-reviewer` | **opus** | Analysis feeding the session's decision | @@ -24,8 +24,9 @@ Routing principle: **explore and execute on the cheaper tiers, decide on the top | `scaffolder` | **sonnet** | Boilerplate generation is mechanical | | `explore` | **sonnet** | The highest-volume agent — routine investigation is Sonnet-fine; bump per-invocation to Opus for genuinely hard blast-radius/architecture work | | `deslopper` | **sonnet** | Deletions are tool-grounded (tldr call graph) and guard-railed (no rm/commit/push, conservative auto-fix) | +| `codex-verifier` | **sonnet** | Independent cross-model check via the Codex CLI — the value is a different model family reviewing the diff, not raw reasoning depth on the Claude side | -The `sonnet` tier is now Claude Sonnet 5 — near-Opus quality on coding/agentic work — which reinforces the split above: `tester`, `scaffolder`, `explore`, `deslopper`, `implementer`, and `reviewer` stay on Sonnet for fan-out/mechanical/execution work at a fraction of Opus cost, while the judgment-bearing agents that gate a decision (`maestro`, `planner`, `oracle`, `security-reviewer`) stay on the top tier. +The `sonnet` tier is now Claude Sonnet 5 — near-Opus quality on coding/agentic work — which reinforces the split above: `tester`, `scaffolder`, `explore`, `deslopper`, `implementer`, `reviewer`, and `codex-verifier` stay on Sonnet for fan-out/mechanical/execution/consult work at a fraction of Opus cost, while the judgment-bearing agents that gate a decision (`maestro`, `planner`, `security-reviewer`) stay on the top tier. Override per-invocation when a specific task warrants it: bump a cheap agent up — `Agent(explore, "...", model: "opus")` for a hard investigation — or drop a decision agent down for a trivial pass. The table is the default, not a ceiling. diff --git a/docs/audits/codebase-audit-2026-07-08.md b/docs/audits/codebase-audit-2026-07-08.md new file mode 100644 index 0000000..0d8878d --- /dev/null +++ b/docs/audits/codebase-audit-2026-07-08.md @@ -0,0 +1,627 @@ +# Adversarial Audit — Codebase Mode + +- **Repo:** cc-settings @ `26dc624` (main) +- **Date:** 2026-07-08 +- **Method:** 9 per-area full-read finder agents (Sonnet) → 9 per-area adversarial verifier agents (Opus, instructed to refute) → team-knowledge reconciliation → single-session synthesis (Fable). 59 raw findings; 2 refuted by verifiers; 1 duplicate merged. A Codex cross-model pass on the 14 highs was attempted per the skill contract but the CLI hung with no output and was killed; per the fail-open rule this report is Claude-only. Re-run later with: `codex exec --sandbox read-only` against `docs/audits/` finding list. +- **Result:** **56 findings** — 14 high / 26 medium / 16 low. 54 CONFIRMED, 2 PLAUSIBLE. +- **Team-knowledge reconciliation:** 1 finding (M — array defaults) matches the documented gotcha `cc-settings-installer-skips-nested-config-defaults`; annotated, severity unchanged (the note itself prescribes the fix). No finding was contradicted by a documented decision. + +## 1. Summary table + +| ID | Sev | Area | Issue | Location | Status | +|----|-----|------|-------|----------|--------| +| H1 | high | agents-profiles-rules | The routing table lists a dedicated `oracle` agent pinned to opus[1m] ("Expert Q&A needs nuance"), but there is no agents/oracle.md — skills/oracle/SKILL.md set | `docs/agent-models.md:19` | CONFIRMED | +| H2 | high | agents-profiles-rules | maestro.md documents a full Agent Teams workflow (mailbox messaging, shared task list, team coordination) in its own body, but its tools: frontmatter grants non | `agents/maestro.md:14` | CONFIRMED | +| H3 | high | agents-profiles-rules | reviewer.md sets isolation: worktree and its initialPrompt diffs origin/main...HEAD to scope the review, but per docs/feature-agents-guide.md's own documented g | `agents/reviewer.md:20-23` | CONFIRMED | +| H4 | high | agents-profiles-rules | security-reviewer.md carries isolation:worktree plus an initialPrompt of automated secret/pattern scans, with the same fresh-origin/main worktree blind spot as | `agents/security-reviewer.md:19-22` | CONFIRMED | +| H5 | high | docs | MANUAL.md's 'MCP servers (core vs optional)' table lists `Sanity` under servers that are 'a **core** set of MCP servers — installed automatically by `setup.sh` | `MANUAL.md:472` | CONFIRMED | +| H6 | high | hooks | buildGitStatus() runs four git operations via Promise.all. Two go through lib/git.ts's runGit() wrapper, which always sets `timeout: GIT_TIMEOUT_MS` (15s) and ` | `src/hooks/statusline.ts:64-79` | CONFIRMED | +| H7 | high | installer | createBackup() only tars ~/.claude/settings.json, CLAUDE.md, AGENTS.md, and ~/.claude.json. cleanOldConfig(), which always runs before the copy phase (both full | `src/setup.ts:212-257` | CONFIRMED | +| H8 | high | installer | installMcpToClaudeJson computes `mergedMcp = { ...teamMcp, ...currentMcp }` — i.e. whatever is already in ~/.claude.json for a given server name always wins ove | `src/lib/mcp.ts:148-152` | CONFIRMED | +| H9 | high | installer | installSettings clones fullComposed.mcpServers into `teamMcp` and mutates ONLY that clone's tldr.command/args/serverInstructions to the resolved engine — but th | `src/setup.ts:501-513` | CONFIRMED | +| H10 | high | scripts | The skill doc promises the handoff captures "Progress", "Decisions made", "Files modified", "Next steps", "Learnings". The actual JSON written by cmdCreate hard | `src/scripts/handoff.ts:76-115` | CONFIRMED | +| H11 | high | scripts | checkpoint.ts scopes its artifact directory per project (`claudePath("checkpoints", project)`, project derived from `git rev-parse --show-toplevel`). handoff.ts | `src/scripts/handoff.ts:24` | CONFIRMED | +| H12 | high | security | `hashHooks()` computes its SHA256 over `(settings as Record).hooks ?? {}` only — every other top-level key in settings.json (`mcpServers`, `per | `src/lib/hooks-fingerprint.ts:56-61` | CONFIRMED | +| H13 | high | skills | Oracle's Compare mode instructs spawning parallel `Agent(explore, ...)` calls, but oracle's own frontmatter (`agent: explore`) routes the entire skill to the `e | `skills/oracle/SKILL.md:6` | CONFIRMED | +| H14 | high | skills | qa's "Fresh-Eyes Gate" is marked mandatory ("Mandatory before declaring a user-reported visual bug fixed") and requires spawning a fresh subagent, but qa's fron | `skills/qa/SKILL.md:4-5` | CONFIRMED | +| M1 | medium | agents-profiles-rules | AgentIsolation is a strict zod enum of only ["worktree", "remote"], but docs/feature-agents-guide.md:363 explicitly instructs users to set isolation: "head" as | `src/schemas/agent.ts:30` | CONFIRMED | +| M2 | medium | agents-profiles-rules | Team Mode Workflow step 10 instructs the agent to call TeamDelete after shutting down scanners, but TeamDelete is absent from the agent's tools list (only TeamC | `agents/deslopper.md:17` | CONFIRMED | +| M3 | medium | agents-profiles-rules | The "All Agents in cc-settings" table lists implementer=opus and reviewer=opus[1m], but both agent files pin model: sonnet, and docs/agent-models.md's own routi | `docs/frontmatter-reference.md:85` | CONFIRMED | +| M4 | medium | agents-profiles-rules | README claims react-perf.md loads for `.ts`, `.tsx`, `components/`, or `app/` files, but the actual frontmatter only globs `**/*.tsx`, `components/**/*`, `app/* | `rules/README.md:34` | CONFIRMED | +| M5 | medium | docs | MANUAL.md's 'Guardrails (Always Active)' section states these are 'enforced automatically — no skill needed', including 'Pre-commit verification — tsc + build + | `MANUAL.md:492` | CONFIRMED | +| M6 | medium | docs | The canonical 'Configured Hooks' reference (meant to be the authoritative per-event list of what cc-settings actually wires) omits: `quota-steer.ts` (UserPrompt | `docs/hooks-reference.md` | CONFIRMED | +| M7 | medium | hooks | The file's header comment reads "Pre-Edit Validation Hook — PreToolUse guard for Edit/Write calls," but config/40-hooks.json registers it with `"matcher": "Edit | `src/hooks/pre-edit-validate.ts:2` | CONFIRMED | +| M8 | medium | hooks | THRESHOLD defaults to 12 (`Number(process.env.CC_PARALLELMAX_THRESHOLD) \|\| 12`, matching docs/hooks-reference.md's documented default of 12). But both emitted m | `src/hooks/tool-cadence.ts:51` | CONFIRMED | +| M9 | medium | installer | deepMergeUserWins recurses into plain objects so a team-only sub-key can land inside an existing block (the fix for the documented 'installer-nested-default-gap | `src/lib/settings-merge.ts:410-441` | CONFIRMED | +| M10 | medium | installer | A user hook group survives the light-downgrade strip unless it (a) is byte-identical (JSON.stringify) to a group in the full baseline, OR (b) every hook command | `src/lib/light-profile.ts:165-198` | CONFIRMED | +| M11 | medium | schemas | docs/frontmatter-reference.md documents `skills` (list, skills to preload), `background` (boolean, always run as background task), and `allowedTools` (list, ali | `src/schemas/agent.ts:33-56` | CONFIRMED | +| M12 | medium | schemas | Agent/skill/profile `name` fields share one regex that permits a leading digit and trailing/consecutive hyphens (e.g. "3d-", "a--b" both match). Knowledge notes | `src/schemas/agent.ts:37` | CONFIRMED | +| M13 | medium | schemas | Every non-version diff in scan.ts (settings keys, hook events/types, permission modes, mcp transports, agent isolation/memory) compares `upstream/claude-code-ma | `src/upstream/scan.ts` | CONFIRMED | +| M14 | medium | schemas | The Agent frontmatter table lists `effort` as accepting only `low`, `medium`, `high`, while src/schemas/agent.ts's AgentEffort enum (and the identical Profile-f | `docs/frontmatter-reference.md:30` | CONFIRMED | +| M15 | medium | schemas | lint-skills.ts raw-scans the frontmatter block for `<`/`>` with the stated rationale "security restriction, frontmatter is injected into the system prompt." Age | `src/lib/lint-skills.ts:91-98` | CONFIRMED | +| M16 | medium | schemas | The "Registration" section says: "Add `""` to `MANAGED_SKILLS` in `src/setup.ts` (alphabetical order)." But `MANAGED_SKILLS` is no longer defined in src/s | `docs/skill-authoring.md:91` | CONFIRMED | +| M17 | medium | scripts | `const INSTALL = /(bun add\|npm install\|npx add\|pnpm add\|bun i \|npm i )\s/;` — the `bun i ` / `npm i ` alternatives already include the trailing space, and the r | `src/scripts/check-docs-before-install.ts:13` | CONFIRMED | +| M18 | medium | scripts | checkpoint.ts's command switch has `default: usage();` with no exit call, so a mistyped/unknown subcommand falls through to the end of the script and exits 0 (b | `src/scripts/checkpoint.ts:428-446` | CONFIRMED | +| M19 | medium | scripts | `const RETENTION = Number.parseInt(process.env.CLAUDE_LOG_RETENTION_DAYS ?? "1", 10) \|\| 1;` — an explicit `CLAUDE_LOG_RETENTION_DAYS=0` parses to `0`, and `0 \|\| | `src/scripts/log-bash.ts:15` | CONFIRMED | +| M20 | medium | scripts | `runGate` spawns `bun run