| D-01 | 2026-10-06 | One project with two parts: a launcher (per-terminal profiles of plugins, skills and MCP servers) and a catalog (discoverability over an org's git marketplace), same priority. | User decision. The review round called the shared core/ thin (about 200 lines of parsing) and the "profile = bundle" bridge unproven; the parts were therefore sequenced behind evidence (D-19), and the bridge was reopened (D-17). | User; bridge unverified | After Phase 0, or if a track is cut |
| D-02 | 2026-10-06 | Core launcher mechanism: a generated --settings file that masks plugins (enabledPlugins: false), hides standalone skills (skillOverrides), connectors and MCP servers (disableClaudeAiConnectors, deniedMcpServers), then starts claude. Nothing shared is written. | Masking per key confirmed in Stage 0 reported; settings keys documented as valid in any file verified; reviewers confirmed in their experiments reported. Aliases cover most of this (adversary estimate 80-90% reported); the unserved parts are default-deny, composition and the catalog. | reported | A native allowlist or default-deny enabledPlugins, or a task-scoped profiles feature in Claude Code |
| D-03 | 2026-10-06 | Implementation language Go, one module and one binary with packages core, profiles, catalog. | User confirmed. Single static binary, trivial cross-compile, fast launcher startup, good process handling. | User | If the maintainers' skills differ |
| D-04 | 2026-10-06 | R2: GitHub.com, GHE Cloud and GHE Server, with GitHub Actions. CLI first, workflow second; no hard-coded hosts; minimal third-party actions. | User decision. Designed to the lowest common denominator. GHE behavior untested. Release gates are staged (D-19). | User; behavior unverified | When a GHE Server minimum version is known |
| D-05 | 2026-10-06 | R1: macOS, Linux and native Windows; six targets (darwin, linux, windows, each arm64 and amd64); WSL counts as Linux. | User decision. Only macOS tested. First release gates are staged: macOS and Linux first, Windows amd64 once Stage 0 passes there, Windows arm64 best-effort. | User; behavior unverified | Phase 0.4 results |
| D-06 | 2026-10-06 | R3: works with no, partial and strict managed policy; capability-driven; never bypasses policy. Policy detection reads managed-settings sources and the "required by your org" marker; no exit-code probing. | User decision (the org enforces managed settings). Exit-code probing found infeasible by the technical review reported. | User; reported | When the org's managed keys are known |
| D-07 | 2026-10-06 | R4: open source under MIT. LICENSE added (holder Jorge Barnaby, 2026). No org-specific assumptions in code, schemas, defaults or examples. | User decision. Employer approval to open-source and an IP check are still needed before the first public commit. | User | Employer answer |
| D-08 | 2026-10-06 | R5: the tool lives in a public GitHub repo; each adopter keeps profiles and catalog data in its own private repo. The tool repo contains no real org data, only fictional examples and a starter template. No built-in default profiles. | User decision. | User | Never expected |
| D-09 | 2026-10-06 | R6: both an interactive mode (prompts, pickers, wizards) and a flag-based mode. Flags are the contract; prompts only on a TTY; every flow prints the equivalent command; trust is never auto-accepted. | User decision. | User | If an interactive library cannot meet the Windows constraint |
| D-10 | 2026-10-06 | SR1 to SR5 security requirements: closed profile schema, trust the resolved closure pinned by commit SHA (project profiles off by default), no shadowing and protected controls, private verified local artifacts, hardened CI and releases. | Security review: trust model "not acceptable as written" reported. The key claim, that a --settings file can set bypassPermissions, was verified directly verified. | verified for SR1; rest reported | Before a first release (SR1-SR3) and before corporate CI (SR4-SR5) |
| D-11 | 2026-10-06 | Name ccshelf for the project and the command. | claude-profile collided with three existing tools; ccprofiles had its GitHub user taken and many near-identical names. ccshelf free on GitHub, npm, PyPI, crates.io, Homebrew, the Go proxy and the domains checked verified. Brand risk of the cc prefix remains. | verified availability; trademark unverified | Trademark search, winget and pkg.go.dev checks |
| D-12 | 2026-10-06 | Catalog hosting is the adopter's choice; the tool outputs a static directory. | User decision. | User | Never expected |
| D-13 | 2026-10-06 | Profile sources: dir and git first, plugin later. The MVP narrows this to dir only; git sources wait for SR2. | Tool-repo and data-repo split makes a pinned git source the natural fit; the plugin source is untested under policy. | unverified | When SR2 is implemented |
| D-14 | 2026-10-06 | Standalone skills: explicit off-list through skillOverrides, plus guidance to package skills as plugins. No generated --plugin-dir plugin. MCP servers and connectors hidden through settings keys. | skillOverrides works for standalone skills only reported; settings keys are valid in any file verified. | verified/reported | If Claude Code adds a skills allowlist |
| D-15 | 2026-10-06 | Metadata ownership is hybrid: plugin authors write it, the platform team reviews it. | User decision. | User | Adoption experience |
| D-16 | 2026-10-06 | Catalog-only metadata lives in a sidecar file per plugin (catalog/plugins/<name>.toml); single-file mode for small registries. | User decision, but the original rationale (CODEOWNERS cannot route per JSON entry) is incomplete: CODEOWNERS can route plugins/*/catalog.toml too. Kept for fewer merge conflicts and easier editing, with owner to be derived from CODEOWNERS (see O-09). | User; rationale reported | Phase 1 catalog work |
| D-17 | 2026-10-06 | Generated profile bundles are committed in bundles/ with a drift check; leave version out so the commit SHA is the version; no tag-plugins.yml until version ranges are used. Reopened: whether "profile = bundle" holds is pending T7. | Reviewers: masking a dependency of an installed bundle disables the bundle and raises errors reported; tags only matter for version ranges reported. T7 untested. | unverified | Phase 0.3 results |
| D-18 | 2026-10-06 | Profiles share auth, history, memory and the plugin cache (no CLAUDE_CONFIG_DIR). Accounts are a separate axis, combined by honoring the environment, optional built-in accounts, or an existing tool. | User decision (shared state is fine; document how to combine). Auth under the flags used confirmed reported. Second-account test parked. | User; reported | When a second config directory is available |
| D-19 | 2026-10-06 | Direction: evidence first, then trimmed scope. Phase 0 evidence, Phase 1 MVP, Phase 2 deferred. | User chose it after the adversarial review (product reviewer: about 10% chance the plan should proceed unchanged reported). | User | Phase 0 results |
| D-20 | 2026-10-06 | Exec on Unix, spawn on Windows; cache cleanup by age only; settings JSON validated before every launch. | An invalid settings file is ignored silently verified; spawn-and-wait breaks Unix job control reported; settings carry through to background sessions reported. | verified/reported | Linux and Windows Stage 0 |
| D-21 | 2026-10-06 | Docs layout: README with glossary, this decision log, design/ and research/; the HTML report is generated from the Markdown by docs/build_report.py and is never edited by hand. | The audit found drift between eight notes and a hand-mirrored report; the ops reviewer proposed this layout. User decision. | User | If the generator becomes a burden |
| D-22 | 2026-10-06 | Full implementation directed. The user's /goal directs implementing the whole application (launcher, catalog, CI and release workflows) with fresh sub agents and Opus as adversarial reviewer. The Phase 0 evidence items (O-04 to O-07, O-10) stay open; code was written ahead of them and must be re-checked against their results. | User directive. Builds on D-19 without superseding its evidence list. | User | Phase 0 results contradict a design assumption |
| D-23 | 2026-10-06 | Environment policy (SR1): the launcher passes only CCSHELF_VAR_<NAME>, names ending _REF, and CCSHELF_PROFILE from a profile; a denylist (including GITHUB_, RUNNER_, ACTIONS_, CI, proxy and loader variables) is applied first. An allowlist, not a denylist. | A security review flagged an incomplete denylist; adversarial review A found the CI-variable gap. verified (tested in internal/envpolicy) | Opus review | A real profile needs another variable |
| D-24 | 2026-10-06 | Account precedence: an explicit --account flag overrides an existing CLAUDE_CONFIG_DIR and prints a note; otherwise the environment is honored. | Explicit intent beats ambient state; the note keeps it visible. Implemented in config.ResolveAccount. | Medium | User feedback |
| D-25 | 2026-10-06 | Superseded by D-38. Module path was provisional until a GitHub home was chosen (O-08). | The ccshelf org name was free but unregistered. | Medium | Done: see D-38 |
| D-26 | 2026-10-06 | Closed schemas are exact-case. Go's TOML and JSON decoders match keys case-insensitively, so every parse of profiles, the registry, config.toml and plugin.json also checks the exact spelling (internal/config/tomlkeys). The closure hash is version 2 and treats profile controls (env names, includes, excludes, inherit flag, account) as a risky item shown in the trust review. | Adversarial reviews B and C proved case variants bypassed the closed schema and hid hooks. verified | High | Go decoders change |
| D-27 | 2026-10-06 | Trust binds to what was reviewed: Accept takes the closure hash the reviewer saw; git sources read blobs from the object store at the pinned SHA instead of a filtered checkout; registry arguments are shown verbatim in the review. Confirmations use a typed yes with no default. | Adversarial review F (filters, redaction hiding commands) and E (unsafe defaults). | High | Opus review |
| D-28 | 2026-10-06 | Protected controls fail closed. [protect] lists come from the org config of every shared source (git sources extract ccshelf.toml from the pinned tree), are pinned in the trust closure, and stay enforced from the lockfile when a source is unreachable; protected plugins are written enabled when inherit_user_settings = false; strict MCP degrades to a deny list rather than failing. | Final black-box review X: git sources silently lost protection and inherit=false let a personal profile disable audit plugins. | High | Opus review Y |
| D-29 | 2026-10-06 | run uses the pinned commit and does not hit the network when the trusted checkout is cached; trust and ls --refresh re-resolve tags. One unreachable source is a per-source warning, never a failure of unrelated profiles. | Review X: every command ran git ls-remote, so an outage broke personal profiles. | Medium | Users report stale pins |
| D-30 | 2026-10-06 | Cached git checkouts are verified down to every tree object (replace refs disabled) before each use; when a source is unreachable the union of all pinned protect lists keeps being enforced and the newest verified cached checkout is used; plugins that own a protected MCP server are protected (all same-name candidates, with a warning). | Review Y: replace refs could swap a tree behind a pinned commit; an outage dropped protect lists of protect-only sources. | High | Next adversarial review |
| D-31 | 2026-10-06 | Plugin sources are bound to their real marketplace source (from claude plugin marketplace list --json, verified for github, git and directory kinds) in the source id and trust key, with an optional marketplace check in the config; search, recommend and doctor --policy can use the cached org source offline; PRF002 is info for cmd /c npx|node|uvx|bunx name@version (the documented Windows launcher) and a warning for anything else. | Review X/Z2: the marketplace check was a dead hook; git-source users had no catalog; the starter failed lint --strict. | Medium | Review Z |
| D-32 | 2026-10-06 | Marketplace identity is kind-tagged and host-neutral (github:owner/repo, git:host/path; ssh, https and scp forms of one repo are equal; directory sources are hashed, never a machine path). Git checkouts carry a layout version (bump layoutVersion when the watched file set changes). Catalog reads (search, recommend) use only a commit the user accepted (the lockfile commit, or the exact SHA when the ref is one). The PRF002 downgrade is limited to the exact cmd /c <npx|uvx|bunx> [-y] <pinned name@x.y.z> shape. | Review Z: kind confusion, a hard-coded host, an upgrade that would report valid caches as tampered, catalog reads of untrusted commits, and a downgrade that hid -c/-e/UNC forms. | High | Next adversarial review |
| D-33 | 2026-10-06 | Recommendation (for the user to review): build ccshelf; do not adopt either existing tool, and do not contribute structural changes; take specific ideas from both. Neither fuzzyalej/claude-profile (additive --plugin-dir vendoring, no masking of installed plugins, no managed-policy, trust or protected-plugin model, first-match profile search across project and personal sources) nor edimuj/claude-rig (separate CLAUDE_CONFIG_DIR and symlinks, free-form settings, Windows needs Developer Mode) does default-deny masking on one shared plugin store with R3 and SR1 to SR5. Both are single-author projects (10 and 9 stars; claude-rig quiet since 2026-06-08). Ideas to take: find index, multi-profile launch, --frozen lock check, statusline snippet, --json and doctor --fix. Kill and obsolescence rules are in the roadmap (Phase 0.6). Closes the Phase 0.2 part of O-04 (see D-22); Phase 0.1 (routing eval) still decides whether the launcher ships as a product. | Read-only review of both repositories on 2026-10-06 (README, docs, launch code, workflows, gh metadata); nothing was built or run, so run-time behavior is unverified. See adopt-or-build.md. | Medium | Either project adds settings-based masking, managed-policy handling and trust pinning, or a maintainer asks for them; a new tool above 100 stars covers the gaps; the routing eval fails (then the answer becomes the alias recipe, K1) |
| D-34 | 2026-10-06 | Superseded by D-45. The website (site/) was built and checked but not published automatically. The pages workflow runs only by workflow_dispatch; the site address and the repository link are filled in at deploy time (scripts/build-site.sh), and the repository link defaults to the private working repo until a public home exists. | The repository is private and the GitHub org ccshelf is unregistered, so an automatic deploy would publish a page that links to a repository nobody can open, and open-sourcing is not yet approved. Gated on O-11, O-08 and D-25. Setup and how to turn automatic deploys on are in site/README.md. | High | O-11 and O-08 are settled, or Pages for a private repo is wanted |
| D-35 | 2026-10-06 | The Markdown documentation is published on the website as a /docs section, rendered at build time from docs/**/*.md; no static-site generator, and the deploy is still manual (D-34). scripts/build_docs.py (Python standard library, reusing the parser of docs/build_report.py) writes static pages in the site's design with a sidebar, per-page table of contents, heading anchors, copy buttons and a client-side search over a generated index; the CLI reference page is generated from the real binary (scripts/gen-cli-reference.sh, --check in CI). Links between notes map to the generated pages, other repository files link to REPO_URL/blob/main/..., and external links are kept only for the repository and code.claude.com; the three report widgets need a script the CSP forbids, so a note points to docs/report.html, which is not published. Generated HTML is never committed: scripts/build-site.sh builds dist/site and scripts/check_site.py validates that built output. | User decision: reuse what exists (the Markdown, the report's renderer, the site's design and checker) rather than add a generator or a dependency. The report stays the interactive view; the website gets a readable, searchable, linkable copy of the same sources. Third-party links in the research notes are shown as text with the address, because the checker forbids links to hosts other than the repository and the documentation host. | High | The notes outgrow the single-pass renderer, or the Markdown subset has to grow |
| D-36 | 2026-10-06 | Release model: a bot-maintained release pull request from conventional commits (googleapis/release-please-action v5.0.0, pinned by SHA). Merging it creates the tag and a draft release; release-please.yml then dispatches release.yml with the tag as ref, so the signing identity stays release.yml@refs/tags/<tag>, and the existing signed goreleaser pipeline publishes. 0.x: breaking and feat bump the minor, fixes the patch; first version 0.1.0; no tag is created by hand. No PAT or App key by default; the GitHub App variant is the documented hardening. Corrected after adversarial review: verify is read-only and the draft-release checks (exactly one draft, name and tag_name equal the tag, target commit equals the tag's commit) run in the release job, because the REST API shows drafts only to tokens with push access; versions are forced with release-as in the configuration through a fix:/feat: titled pull request (a hidden-type commit with Release-As: produces no release), not with a footer in a description. | User decision ("release PR from conventional commits"), after reading the Changesets setups of ccpod, cc-analyzer and pi-harness-delegate (same version-PR idea, but a change file per pull request, CI never runs on the version PR, notes differ from the changelog). Mechanics read from release-please v17.6.0 and goreleaser v2.18.2 source; tags and PRs made with GITHUB_TOKEN start no workflow except workflow_dispatch. Trust change documented in release.md. verified for the source reading, unverified for the end-to-end behavior: no real release has run. | Medium | The first real release; a ruleset or GitHub change that lets Actions bypass tag rules (then restrict tag creation); immutable releases becoming mandatory; the schemas stabilise (1.0.0) |
| D-37 | 2026-10-06 | Pull request titles are enforced as Conventional Commits by the separate workflow pr-title.yml (a second required check next to ci-ok, so title edits never re-run the CI matrix): types feat fix docs test ci build refactor perf chore revert, optional scope and !, lowercase description, no trailing period, at most 72 characters (120 for Dependabot). The title, description and author arrive through env: only; invisible and control Unicode characters are rejected; chore(main): release ... is reserved for github-actions[bot]. docs, test, ci, build, refactor and chore are hidden from the changelog and do not trigger a release. The squash default must be "Pull request title", and a description containing BEGIN_COMMIT_OVERRIDE, BEGIN_NESTED_COMMIT or a Release-As: line is rejected. | Maintainers squash-merge, so the title is the changelog entry and the version bump; a misspelled type would be silently ignored by release-please. The title is not the only input: release-please also reads the pull request description of the merged commit (override blocks, Release-As:, paragraphs that look like commits), so the description must not reach main and the release pull request must be reviewed entry by entry {R: from the release-please source, not re-checked}. Checked by scripts/check_pr_title.py with unit tests. | High | If merges stop being squashed, or release-please is replaced |
| D-38 | 2026-10-06 | The public home is github.com/yorch/ccshelf; the Go module path is github.com/yorch/ccshelf. Every ccshelf/ccshelf reference (module path, imports, links, the Action's uses: path, the cosign identity, the SECURITY.md advisory URL, CODEOWNERS) was rewritten. The ccshelf GitHub name stays unregistered, so it can be claimed by anyone; if the project later moves to an org, the module path changes again. | User decision before making the repository public: the unregistered org name could be registered by someone else and receive security reports and traffic. | High | An org is registered and the repo moves |
| D-39 | 2026-10-06 | End-user installers scripts/install.sh and scripts/install.ps1, published as release assets and covered by the signed checksums.txt; verification policy: SHA-256 against the same release's checksums.txt ALWAYS (exactly one line, no way to skip), plus the cosign signature of checksums.txt when cosign is on PATH (a failure is fatal; --require-signature makes a missing cosign fatal; otherwise a warning says the check is integrity only). No sudo, no telemetry, no GitHub API ("latest" is resolved from latest/download/checksums.txt, then everything is fetched from the resolved tag), https only (curl is required: a wget fallback could not pin https for redirects or ignore its config; the installer ignores ~/.curlrc), archive entry allowlist, symlink and ownership checks on the target directory, atomic install followed by a check that the target is a regular file with the verified SHA-256 (--force never installs into or through a directory or link). Details: release.md ("End-user installers") and SECURITY.md ("What the installer verifies"). | User decision on the verification policy; a user-facing installer was missing (the docs said "clone and build"). Snapshot build checked (dist/checksums.txt lists both scripts); tests and about seventy mutation tests run offline; PowerShell run under PowerShell 7 on macOS and in a Linux container (including a local HTTPS server with a self-signed certificate) in a developer copy with two Windows-only checks relaxed, not on Windows. | Medium (nothing has run against a real release; Windows PowerShell 5.1 and real Windows untested locally) | The first release is published (run the one-liners against it); cosign becomes ubiquitous (consider making the signature mandatory); notarization and Authenticode signing exist |
| D-40 | 2026-10-06 | ccshelf update and an opt-in automatic update. The command verifies before it replaces: HTTPS without credentials, redirects limited to the release host and GitHub's release-asset hosts, size and time caps, the archive's SHA-256 against its single line in checksums.txt, and, when cosign is on PATH, the keyless signature of checksums.txt against the exact release-workflow identity (--require-signature makes cosign mandatory); only the ccshelf entry is extracted, into a private file beside the binary that must run and report the target version; the old binary is kept as <name>.old (--rollback; a backup that cannot be run needs --force). The cosign signer identity is the compiled-in repository on github.com, never derived from base_url (a fork sets [update] cosign_identity_repo; [update] asset_hosts adds exact redirect hosts for a GHES); --force never implies a downgrade; --require-signature is checked before anything else; updates serialize on an OS lock (flock/LockFileEx). A binary owned by Homebrew, Scoop, WinGet, go install or a system package, and development builds, are refused without --force (the right command is printed). Automatic: [update] mode = off (default) | notify | install, interval (default 24h, minimum 1h), base_url for GitHub Enterprise Server (https only, validated like the git URLs); no channel. install only takes a newer stable release of the same major version (same minor for 0.x), never a downgrade, never a package-managed or development build, and takes effect on the next run; both automatic modes need a terminal on stdin, stdout and stderr (scripts use update --yes); run and dry-run do no network I/O (cached notice only); CI and CCSHELF_NO_UPDATE_CHECK switch it off; ccshelf init asks once, on a terminal, only when [update] is absent. The e2e tests reach a plain-http loopback server through the compile-time constant config.LoopbackHTTPAllowed, true only with -tags e2eloopback. Supersedes nothing. | User decision (auto-update opt-in, verification always, cosign checked when present). Fits R1 to R4: six targets, no host hard-coded, no policy involvement, no org data; the network exception is the one SECURITY.md documents. GitHub behavior checked once by hand against public releases: unauthenticated API (60 requests per hour per IP) and the 302 from github.com/.../releases/download to release-assets.githubusercontent.com verified. The Windows replacement runs only in CI unverified. | verified design and tests on macOS, unverified Windows | The first release exists and update runs on Windows CI; a real incident; GitHub changes the asset host or the unauthenticated limits; an update --check is wanted by the Action |
| D-41 | 2026-10-06 | ccshelf catalog init [dir] bootstraps a new org data repo or retrofits an existing marketplace repo, non-destructively; ccshelf init stays the per-developer setup. It builds a plan (create, skip-exists, needs-merge, overwrite only with --force after a .bak) and writes only what is missing; a marketplace.json is never rewritten; the second run with the same flags changes nothing. It writes ccshelf.toml, a marketplace skeleton, sidecar stubs, CODEOWNERS, the three workflows (pinned by --ccshelf-ref, a failing guard job, a job of its own, until pinned), README, .gitattributes and .gitignore; no built-in profiles (R5). Placeholders carry TODO(ccshelf) and lint reports them as the new warning CAT048, so the generated repo lints with no errors. The templates are embedded and are the single source of the starter's shared files (a drift test with -update). Details: catalog-and-org-repo.md and cli.md. | User decision on the command name and on non-destructive adoption. Built on the R6 rules (flags are the contract, prompts only on a TTY, equivalent command printed) and the SR4 hygiene (writes confined to the target, no links followed, exclusive atomic creation). Tested with unit, golden, adopt-fixture and end-to-end tests; the generated workflows pass actionlint and check-pins.sh. Not tested: a real GitHub Actions run of the generated workflows. | High | An adopter needs a merge that edits existing files in place; GitHub changes how CODEOWNERS or Pages visibility work |
| D-42 | 2026-10-07 | Restrained CLI polish: task-grouped help and getting-started examples; contextual usage-help hints; wrapped, labeled records when minimum table widths cannot fit; successful empty results and next-step hints on stdout. No banners or emoji, no command or trust changes, and no changes to JSON collection types or exit-code meanings. | User selected overall polish and restrained presentation. The existing root help mixed daily-use and org-maintenance commands; minimum-width tables could overflow narrow terminals; empty results were inconsistent across commands and streams. See cli.md. | High for direction; terminal usability still needs user feedback | Users find the groups or stacked layout confusing, or a supported console exposes a rendering issue |
| D-43 | 2026-10-07 | A user may configure an HTTPS catalog.json URL in [catalog].remote_url; discovery commands use it outside an org repo. The published JSON is data for search and profile recommendations, not a profile source. | User requested remote catalog retrieval. Keep repo-local source files authoritative when present; bound and version-check the downloaded JSON. | User request; implementation details unverified | Private catalog authentication or plugin recommendation metadata is needed |
| D-44 | 2026-10-07 | ci.yml selects jobs by changed paths; main, merge groups and dispatch always run everything. A changes job runs scripts/ci_changes.py and the other jobs are gated on its outputs; ci-ok (which counts skipped jobs as passed reported) always reports because the workflow has no paths: filter. It fails open (full suite) on any event but pull_request, on a change under .github/ or to the classifier itself, on the release pull request branch (release-please--*), on a file no rule knows, and when the diff cannot be computed or is empty. The installer mutation tests moved to their own job (install-mutants, installer changes only); the six-leg build matrix is one job; smoke is a step of test; action-test is a step of action-e2e; the experimental arm64 test legs run off pull requests only (the matrix is generated because a job-level if: cannot read matrix reported); CodeQL scans pull requests only when Go, workflow or Action files change. Details: release.md. | Measured on recent runs: about 30 jobs and 14 to 15 minutes on every pull request, documentation-only ones included; the critical path was install-test (ubuntu-latest), about 12.7 of its 14.7 minutes being test_install.sh --mutants; every other job took 4.5 minutes or less. Classifier tested with unit tests and a dry run over recent commits; the workflow is checked with actionlint. Not tested: a real run on GitHub, which is the first pull request that carries this change (it touches .github/, so it runs everything). | Medium: the rules are hand-kept and a missed dependency would skip a job that should run | A skipped job lets a breakage reach main (add the path to the rule, or drop a gate); a required check other than ci-ok and pr-title is added; GitHub offers native path-aware required checks |
| D-45 | 2026-10-07 | The website is published automatically. pages.yml deploys on every push to main that changes site/, docs/, the site build scripts or the workflow, and stays runnable by workflow_dispatch; pull requests never deploy (the site job in ci.yml validates them). Supersedes D-34. | The repository is public under yorch/ccshelf (D-38), the module path and links are settled, and a stale site is worse than an automatic one; the deploy runs in the github-pages environment with a least-privilege token and no secrets. Employer approval (O-11) remains the owner's responsibility. | High | The site or its build starts to need secrets, or deploys cause trouble |
| D-46 | 2026-10-07 | Interactive Select/MultiSelect prompts use an inline keyboard picker with type-to-filter on capable file-backed terminals, and keep the numbered line prompt everywhere else (--plain, non-TTY/CI/JSON, redirected or non-file streams, unsupported raw/VT consoles, dimensions too small for the key legend). Keyboard prompts emit no color escapes, restore raw input and scoped output state synchronously before returning or spawning claude, and read input by synchronous polling on Linux/macOS and Win32 console events on Windows, so no reader thread is left behind. A mid-prompt resize exits safely with a retry with --plain hint. | User selected the picker direction. The numbered fallback preserves R6's screen-reader/dumb-terminal path and D-42's restrained presentation; restoration before claude starts is R6 rule 6. Real Linux PTY tests prove terminal-state equality after return and that the next reader receives its whole line; the Windows ABI/decoding tests are cross-compiled only. | High on Linux; unverified native macOS/Windows | A supported console shows a rendering or restoration defect, or a native macOS/Windows smoke test fails |
| D-47 | 2026-10-07 | The full init and new wizards print a sanitized summary and ask a default-no write confirmation (Write this configuration? / Create this profile? (does not grant trust)); the command-local --yes confirms writing only, never trust. init also replays an explicit --update-mode <effective> --yes (plus --force/--config), so its equivalent command runs prompt-free; new replays --scope, --yes and --no-interactive with its content flags. Flags beat prompts: a partial-flag run asks nothing, and --yes alone neither starts nor suppresses content gathering. Decline, abort and cancellation before the gate write nothing; a declined confirmation is a failure (exit 1, message per command, as catalog init does), while an abort or cancel keeps the interrupted code. | User approved the safe default and directed that a confirmation is never weakened to keep a fixture green. R6 rules 4-5 (equivalent command printed, trust never auto-accepted) and the same default-no confirmation used by catalog init (D-41) and update (D-40). | High | A wizard gains a step that cannot be replayed, or a default proves unsafe |
| D-48 | 2026-10-07 | ccshelf new writes one TOML per profile: user scope (--scope user, default) into the user profiles directory, or --scope project into the nearest Git working-tree root /.ccshelf/profiles (cwd outside Git). Writes are exclusive 0600 in newly created 0700 directories, below an opened-directory anchor that refuses symlinks and replacement ancestors (a project destination must be a plain directory chain; the personal config root may be reached through symlinks, matching the reader, while the profiles directory below it must be a plain directory; the writer resolves the root and refuses symlinks at or below it); the candidate is resolved in memory under the normal origin/parent/MCP rules before any write, and creation never enables, configures or trusts project profiles. A full wizard offers the location; an explicit --scope skips only that question. Creation fails closed when it cannot prove the new name does not shadow an unavailable namespace (a git source without a verified cached checkout, or a plugin source with no offline namespace cache); user creation still succeeds with per-source warnings (an uninspectable local namespace, such as a symlinked .ccshelf or an unsafe .git marker, is warned about the same way), and collision checks never fetch. | User approved project scope and the creation-does-not-grant-trust model (SR2: project profiles off by default, never shadowing). Reuses the existing PersonalDir()/XDG/APPDATA policy rather than redefining it, and leaves the runtime resolver's shadow rules unchanged. Symlink and os.Root confinement are verified on Linux; native Windows reparse-point behavior is cross-compiled and linted only. | High on Linux; unverified native Windows/macOS | A user needs in-place profile editing, or a native Windows/macOS check shows a confinement gap |
| D-49 | 2026-10-07 | For a closure that includes a non-personal (org or project) source, any change to the resolved closure hash needs trust again, including identity-only fields (description, when_to_use, model, effort): the state is Changed, and the launcher asks interactively or exits 4 non-interactively. Risky (every profile-controls item, registry entries, prompt text, plugin includes; only the identity item is not risky, consistent with D-26) only orders and marks the diff; it never gates acceptance. A personal-only closure is always trusted. With [trust] on_change = "fail" a Changed or TagMoved closure exits 4 even on a terminal, while a New closure still prompts on a terminal. show --json and diff output are unchanged. The docs that said only risky changes need re-trust (profiles.md trust model step 3, the [trust] on_change comment, security.md, cli.md) are corrected, not the code. | internal/trust/verdict.go CheckWithProject (state Changed whenever the hash differs) and internal/cli/launcher/run.go settleTrust (prompt or exit 4); R6/SR2 (trust is never auto-accepted). Supersedes the "risky only" wording in earlier design text. | High | Re-trust for cosmetic edits becomes a real burden: consider taking identity-only fields out of the trust hash rather than auto-accepting |
| D-50 | 2026-10-07 | ccshelf config changes an existing configuration in place: path, show, source ls/add/pin/rm, set, unset, edit, and a menu in a terminal. init stays the only creator. set/unset take an explicit allowlist (trust.on_change, trust.require_pin, trust.trust_project_profiles, update.mode, update.interval, catalog.remote_url, default_account, ui.color, ui.interactive); accounts, sources, claude.path and the update source are refused (exit 2) with a pointer to the dedicated command or config edit. A write shows before and after, asks a default-no question in a terminal, keeps config.toml.bak, refuses when the file changed since it was read, and re-encodes the file (comments are lost, and the summary says so). Weakening changes (require_pin off, trust_project_profiles on, update.mode = install, a git source not pinned to a tag or full commit) need --yes without a terminal and always warn. Nothing in the group fetches a source or records trust. | internal/config/edit.go (allowlist, Weakening, SaveChecked), internal/cli/launcher/config*.go; R6 (flags first, prompts optional), SR1/SR2 (no trust, no auto-accept), D-47 (default-no write confirmations). Tests: go test ./internal/config ./internal/cli/launcher ./internal/e2e -run Config. | High | Users need comments kept: switch to a comment-preserving TOML edit; or a weakening rule proves too strict or too loose; or account add/rm should share the backup and changed-file check |
| D-51 | 2026-10-07 | A dir source path may not start with $PWD, ${PWD}, $OLDPWD or ${OLDPWD}: configuration validation refuses it on every load (a hand-edited config fails closed with the reason), and config source add refuses it with exit 2. Any other $-prefixed dir source added through config counts as weakening (needs --yes without a terminal, warns that the variable is read at every run). config also treats a change of claude.path, update.base_url, update.cosign_identity_repo, a new update.asset_hosts entry, a plugin marketplace or an existing git URL as weakening, compares git sources in normalized form (another --path of one repository is allowed), writes paths unexpanded, and diffs the file against the exact bytes to be written. This can break an existing config that used $PWD; that is intended (SR2/SR3): a cloned repository's .ccshelf/profiles would otherwise be an org-kind source. | internal/config/config.go (checkDirSourcePath, ParseUnexpanded), internal/config/edit.go (Weakening, NormalizeGitURL); review of D-50. A run-time refusal of any dir source inside the current directory's repository was not added: the working directory is only known at run time and the check would need a git lookup on every command. | High | A real use needs a cwd-relative source: add an explicit, per-repository trust path instead of a variable; or the run-time repository check proves cheap |
| D-52 | 2026-10-08 | An org data repo can be profiles-only: [catalog] enabled = false in ccshelf.toml (default true, so existing repos are unchanged) says the repo has no marketplace, sidecars, bundles or catalog. ccshelf lint then skips every check that needs them (CAT001, CAT0xx metadata, ownership of plugins, CAT050 to CAT053) and keeps the profile checks (PRF*), the MCP registry, prompts, protect lists and the CODEOWNERS and platform-owner checks for profiles and ccshelf.toml; a marketplace file or bundles/ that exists anyway is CAT061 (warning) and other [catalog] keys set alongside are CAT062 (warning). compile and catalog build exit 1 with [catalog] enabled = false in ccshelf.toml: this repo has no marketplace, so there are no bundles/catalog; search and recommend do the same when nothing else supplies a catalog, but inside such a repo they first fall back to the user's configured remote catalog or org source (exit 0), because a profiles-only directory is not a catalog source; doctor still checks profiles and policy (it prints a note, and --json lists the catalog checks as skipped, DOC000, with the reason); sidecars and marketplace files are read from no source with the catalog off (LoadSidecars, LoadTaxonomy and the launcher's deprecation warning), so nothing outside the watched set is read (SR4); a dir or git source with the catalog off is not taken for a catalog source, and a git source does not watch sidecars or marketplace files. ccshelf catalog init --profiles-only writes ccshelf.toml, CODEOWNERS, a profiles-only README, .gitattributes, .gitignore and a lint-only validate.yml (same SHA pins) and no marketplace, sidecars, catalog.yml or release.yml; with it --marketplace-name, --owner and --sidecars stub are usage errors (exit 2), as is an existing marketplace file (the default one or one the config lists), an existing ccshelf.toml without enabled = false (add it yourself, or --force replaces the file with a .bak) and --no-config unless the existing config already has it; running init without the flag on a config that has enabled = false is a usage error too, so a re-run cannot add a marketplace. | Scratch reproduction on 2026-10-08: catalog init dir --no-marketplace --no-sidecars --no-workflows still demanded --marketplace-name and printed next steps for catalog build and compile; ccshelf lint on such a repo failed with CAT001 cannot read marketplace file and one CAT050 profile "x" has no bundle entry "profile-x" per profile, so the validate workflow could not be used; the launcher (dir source, and git source by reading internal/profile/gitsource) already worked. Tests: go test ./internal/orgconfig ./internal/catalog/lint ./internal/scaffold ./internal/cli/orgcmd ./internal/profile/gitsource. No Claude Code behavior is involved. SR1 to SR5 are unchanged (no new setting reaches the generated settings; the workflow keeps its SHA pins and read-only token). Interactive init does not ask whether the repo is profiles-only (that would add a prompt to every scripted wizard run): pass --profiles-only, which the printed equivalent command keeps Amended by D-57: the wizard now asks, and an existing enabled = false selects profiles-only instead of a usage error. | High | An org wants plugins later (remove enabled = false and run init without the flag); or the interactive wizard should offer profiles-only; or search should work from a remote catalog of a profiles-only source |
| D-53 | 2026-10-08 | Text that people and agents read follows the structural rules of ASD-STE100 Simplified Technical English (Issue 9, 2025). Strict mode applies to CLI messages and help, lint messages, schema and Action descriptions, and AGENTS.md. STE-flavored mode applies to the other docs, the site and new decision rows. Contributors change text when they edit it for another reason, with no bulk rewrite. docs/research/ and existing decision rows are out of scope. The STE dictionary is not used, and the text is never called STE-compliant. CI does not check it. The rules are in AGENTS.md, "Writing style". | User decision (option A of an assessment: a writing convention, no tooling). ASD does not permit reproduction of the standard without written authority, so the dictionary cannot be in an MIT repo (R4) reported. The source is the asd-ste100 skill, which quotes Issue 9, page 2. The official FAQ returned 403. On 2026-10-08 the skill's regex linter found about 3 to 5.5 findings per 100 words in docs/, mostly semicolons, long sentences and possible passive voice. | User, license reported | The project wants a CI check (the pinned linter with a baseline that can only go down), the routing eval tests STE-style plugin descriptions for catalog lint rules, or a rewrite loses meaning |
| D-54 | 2026-10-08 | One-time rewrite of existing text to the D-53 rules, as an exception to its "change text when touched" rule. In scope: CLI help and messages, lint messages, schema and Action descriptions, scaffold templates, the root Markdown files, docs/design/, the site, and Go doc comments in STE-flavored mode. Out of scope: inline comments, docs/research/ and existing decision rows. The same pull request also removes repetition: each fact keeps one full home (design docs for detail, SECURITY.md for what a release and the installer verify, AGENTS.md for every agent rule), and the other places link to it. After the rewrite, the D-53 rule applies again. Amends D-53. | User decision. One reviewed pull request is cheaper than a slow drift. The change keeps every identifier, flag, exit code and machine-readable output. Tests and goldens that check message text change in the same commit. | User | A reviewer finds that the rewrite changed meaning, or a script depends on the old message text |
| D-55 | 2026-10-08 | A git profile source can track a branch through an explicit branch = "<name>" key, exclusive with ref (exactly one is required). The name follows git check-ref-format --branch rules, checked in Go, and a full ref, a leading - and .. are refused. A branch is allowed with trust.require_pin = true, because the key is explicit. A ref that names a branch still fails, and its message points to branch. Trust stays per commit: ccshelf resolves refs/heads/<name> with git ls-remote to a full commit, and the lockfile ref is branch:<name>, so a branch and a tag with one name never collide. A new commit is a TagMoved update that needs trust again (D-49), and on_change = "fail" exits 4. run uses the trusted commit with no network (D-29). Each profile runs the commit it trusted, from the cache, and a missing checkout is fetched by that commit id, never by the head. At most once per trust.branch_check_interval (default 24h, 1h to 8760h) it checks the head with one ls-remote. A terminal shows the trust diff and asks. Without a terminal, with --yes or with on_change = "fail", ccshelf keeps the trusted commit and prints one line that names ccshelf trust. A failed check warns once, never blocks the run, and is recorded in the private cache. run --refresh, dry-run --refresh, trust and ls --refresh check the head now. init --branch, config source add --branch and config source pin --branch are new. Adding or switching to a branch is a weakening change (D-50) for config, which needs --yes without a terminal. init prints the same warning and keeps its --yes meaning. A tag source that becomes a branch source at the same commit has its lockfile refs rewritten by Store.Rekey (equal hash, same URL and commit only). The --json source fields keep their format, and branch data is in tracks. Tag and commit sources are unchanged. | User decision, 2026-10-08. internal/profile/gitsource (ResolveHead), internal/config (ValidateBranch, TrustRef, Weakening), internal/trust/verdict.go, internal/cli/launcher/branch.go. Tests with local bare repositories and a fake remote: go test ./internal/config ./internal/profile/gitsource ./internal/trust ./internal/cli/launcher -run Branch. Design: profiles.md, security.md SR2. | High (the rules); Medium (the 24h default and the question in a terminal) | A team pushes to the branch without review (then branch protection matters more than this feature); users find the 24h check too slow or too noisy; a tracked branch needs a signed-commit check; or the launcher gets a background update check that replaces this one. Amended by D-58: a commit that another profile accepted is offered to the other profiles of the source at once |
| D-56 | 2026-10-08 | A justfile replaces the Makefile for local developer tasks. The Makefile is removed, with no forwarding shim. The recipes keep the target names, the pinned tool versions and the -ldflags version stamp. You can override a variable with just version=1.2.3 build or the environment. test and test-race take packages and flags. CI does not change: its workflows call the scripts and go directly, and they never called make. | User decision. just gives recipe arguments and a recipe list, with no .PHONY list and no tab rules. The cost: contributors must install just 1.15 or later (the first release with env()). make comes with macOS and most Linux systems. The recipes still need bash, so native Windows needs Git Bash with either tool. The port also changes ci: a failing actionlint now fails it. The Makefile used an echo fallback that hid the failure. | High | just becomes a burden for contributors (installation reports in issues), or CI needs a shared task runner |
| D-57 | 2026-10-08 | Amends D-52 (the wizard, and the rerun rule). In a terminal, when --profiles-only is not given, catalog init asks "Does this repo hold only profiles (no plugin marketplace or catalog)?" before the marketplace-name question. The default is no, the full repo as before. An answer of yes skips the marketplace-name, owner and sidecar questions, as the flag does, and the printed Equivalent: command includes --profiles-only; the next steps are those of a profiles-only repo. The question is not asked in adopt mode when a marketplace exists (the default file or one listed in catalog.marketplaces), when --marketplace-name, --owner, --sidecars stub or --no-config is given, or when an existing ccshelf.toml without enabled = false would not be replaced (the answer yes would be a usage error). An existing ccshelf.toml with enabled = false now means profiles-only in the plan, with or without a terminal: the usage error of D-52 for a run without the flag no longer applies. This only removes files from the plan. The command prints the note ccshelf.toml has [catalog] enabled = false, so ccshelf uses --profiles-only, and a printed equivalent command includes the flag. On that path a flag or a file that needs a marketplace (--marketplace-name, --owner, --sidecars stub, an existing marketplace file) is a usage error whose text names the config and does not say that the user passed --profiles-only. Without a terminal there is no question, and the flags and the config decide. | User follow-up to D-52: a user who does not know the flag always got a full catalog repo. Review round: the inference must not depend on the terminal (R6). Code: scaffold.ChooseProfilesOnly, inferProfilesOnly and Plan.ProfilesOnly in internal/scaffold/plan.go, and runCatalogInit. Tests: go test ./internal/scaffold ./internal/cli/orgcmd -run 'ProfilesOnly'. No security change (R6: the equivalent command is printed, nothing is accepted for the user). | High | Users find the extra question noisy in scripted wizard runs |
| D-58 | 2026-10-08 | Amends D-55. When a profile runs at a commit of a branch source and the trust lockfile holds a commit that another profile accepted more recently (by record time) for the same source and branch, ccshelf offers that commit to this profile at once: no ls-remote, no network call to find it, and whatever trust.branch_check_interval says. It offers the commit only when the commit in use is a strict ancestor of it, proven offline on the verified cached checkout of the candidate (the parent line of the commit object, or git merge-base --is-ancestor, with the same git hardening as the other calls). When the proof is not possible (no checkout, a shallow history, an error), ccshelf makes no offer and leaves the commit to the periodic check. A newer time alone is never enough: a rollback or a force-push leaves an older or unrelated commit as the newest record. In a terminal, without --yes and with on_change = "prompt", it shows the trust diff of this profile at that commit and asks. The text names the commit and the profile that trusted it, and never says that the branch moved, because no head was read. Accept runs the commit, decline runs the profile's own trusted commit. In all other cases it runs the profile's own commit and prints one notice line, whose hint is ccshelf run <profile> in a terminal (ccshelf trust reads the head and can show another commit). The commit is read from the cache. A decline is remembered per profile, branch and commit in branch-check.json (private file, strict decode, at most 64 entries): until the commit changes or one trust.branch_check_interval passes, only the notice is printed. When the periodic head check is due, it still runs; the remote head then decides, and the elsewhere offer is dropped for that branch. The profile never runs a commit that it did not trust: a commit held in the lockfile only for another profile is not trusted for this one. | User follow-up to D-55: after profile A accepted a new commit, profile B (still trusted at the old commit) heard nothing until the next interval. Review round: the first version could offer a rollback, hid the head check while an offer was open, and asked again at every run. Code: acceptedElsewhere, checkAcceptedElsewhere and noticeOrOffer in internal/cli/launcher/branch.go, Source.IsAncestor in internal/profile/gitsource/cached.go, Verdict.AcceptedFor in internal/trust/verdict.go. Tests: go test ./internal/cli/launcher ./internal/profile/gitsource ./internal/trust -run 'AcceptedElsewhere|IsAncestor|BranchState|AcceptedForAnother'. SR2 is unchanged: trust stays per commit and per profile, and nothing is accepted for the user. | High (the rules); Medium (the proof works only for a direct parent, or for history the cache holds: the cached checkouts have depth 1) | The depth-1 cache makes the offer too rare (then a bounded fetch of the history is an option); or a profile should be able to opt out of offers |
| D-59 | 2026-10-09 | A profile can carry instructions: [instructions] files = [...] (paths below prompts/, at most 32) and inherit (default true). The launcher merges the files along the extends chain (a profile appends after its parents, inherit = false drops its ancestors' files, same source and path appears once, 64 KiB per file and in total), refuses @ import tokens (raw-text rule), joins the files into one CLAUDE.md in <cache>/instructions-<sha256>/, and starts claude with --add-dir <that directory> and CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1. It checks the directory before each launch and rebuilds it when it differs. Project profiles may not set the table (SR2). inherit = false drops the files of the ancestors of the profile only, and the result does not depend on the order of extends. The import rule looks at raw characters with no code span or fence exemption (an @ is allowed only after a letter or digit, or after an odd number of backslashes), because a check that follows Markdown structure missed inputs that Claude Code imports. The generated CLAUDE.md starts with a fixed header line. Every effective file and the header are closure items, and the join order is part of the closure hash. The directory is read-only (0500, file 0400) and still checked at every launch. session.append_system_prompt_file stays unchanged and independent. claudeMd in settings, output_style and claude_md_excludes are not part of this change. | User decision on the name, the inheritance and the subagent requirement. Evidence: instructions-stage0.md. --append-system-prompt-file did not reach a general-purpose subagent verified. --add-dir with the variable reached the main conversation and the subagent, also with --setting-sources project,local verified. claudeMd in --settings was ignored verified. The cost: Claude can edit the added directory, and the variable also affects directories that the user adds. Design: profiles.md, launcher.md, security.md. | Medium. -p mode is verified. Interactive mode, path-scoped rules and the Explore and Plan agents are unverified | Claude Code adds a settings key for CLAUDE.md-style text outside managed scope, or a flag that appends text to subagent prompts in interactive mode. Or an interactive check shows that the route does not load, or Claude Code changes --add-dir or the variable. |
| D-60 | 2026-10-09 | The launcher writes CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1 into the env of the generated settings file when a profile has instructions, and keeps it in the process environment. A profile cannot set it (the profile env allowlist still refuses CLAUDE_CODE_ names). The settings validator accepts that one name with the value 1 only. A profile without instructions gets the same settings bytes as before. Managed settings can turn the variable off, and the launcher cannot override that (R3). The policy reader keeps one flag for this variable (never the value), and the launcher warns at launch when it is set off. In -p mode without compaction, Claude Code did not re-read the instructions CLAUDE.md during a session. After compaction it may re-read it from disk unverified. The read-only directory and the check before launch repair the file for the next launch. The managed detection is best effort: it does not see server-managed settings, and under managedSourcesBehavior: "merge" it lets a higher tier env map replace a lower tier map. | Stage 0 on Claude Code 2.1.296: a project settings.json env value of 0, false or empty turned the loading off despite the process variable. The --settings env beat project settings and also worked alone. A mid-session overwrite of CLAUDE.md did not reach the main conversation or a later subagent (two short runs, no compaction). Docs: managed beats --settings beats project beats user. See the addendum. Refines D-59. | Medium. -p mode only. A user settings.json value of 0 is unverified (inferred, not run). Managed behavior comes from the docs verified, not from a run. | Claude Code changes the settings precedence or the variable, an interactive run shows other behavior, or the launcher can read managed env in a way that covers more cases. |
| D-61 | 2026-10-09 | A profile can set the Claude Code output style with session.output_style. The launcher writes it as outputStyle in the generated settings file (the key joins the closed allowlist). The name matches ^[A-Za-z0-9]([A-Za-z0-9 ._:-]{0,62}[A-Za-z0-9._-])?$ and is checked in the manifest parser, in the schema and in the settings validator. Inheritance is the same as for model: a child replaces its parent, and an unset value inherits. The value is part of the controls item of the closure, which is always risky, because a custom style can replace the coding instructions of the system prompt (omitted when empty, so a profile without the key keeps its hashes). The profile pins a name, not content: a style file with the same name in a project shadows a built-in style. A project profile may set it, as it may set model, because a repository can already set outputStyle and add style files itself, so it gains nothing. The resolver warns when the value equals a built-in name other than Default only without case. The launcher does not look for custom style files. | Evidence: output-style-stage0.md. The key in --settings set the style, and it won over the project setting verified. An unknown name gave no error verified. The docs say the value is case-sensitive and that an unknown name gives the Default style verified. A style applies to the main conversation and to forks only verified. A project style file shadowed the built-in style of the same name verified. A plugin style needs the name <plugin>:<Name> verified. Design: profiles.md. | Medium. -p mode is verified. Shadowing by user and plugin files, the managed-policy precedence, force-for-plugin and interactive mode are unverified or reported | Claude Code adds a flag or changes the name rules or the precedence. Or a check shows a plugin style name that the pattern refuses. |
| D-62 | 2026-10-09 | ccshelf account add and account rm now save the configuration the same way as config: paths keep the text that the user wrote, the previous file is kept as config.toml.bak (0600), and the write is refused when the file changed since it was read. account add --default writes once. Existing entries and an explicit --dir keep their text, and the default directory is written as an absolute path. A repeated account add with no change writes nothing. A missing file is created by account add without a backup, and the create is refused if the file appeared since the read. | Closes the gap that D-50 noted. Before, both commands expanded $VAR in paths and kept no backup. Code: loadConfigForWrite in internal/cli/launcher/session.go, Options.Save and Options.StoredDir in internal/account, config.CreateNew. Tests: go test ./internal/cli/launcher -run Account. | High | Comment-preserving TOML edit replaces the re-encoding, or init and account add share one creation path |
| D-63 | 2026-10-09 | show --json and diff use the effective value of an MCP field that a profile does not set. mcp.claudeai_connectors is always present and is keep when unset. mcp.strict is false when unset. A new mcp.defaults array always lists the fields that fell back to a default, in the order claudeai_connectors, strict. diff shows no line for unset against the explicit default, and shows a real change as, for example, keep -> none. The trust hash and the settings file still read the raw values. | User decision. Text show already printed (default). Unset behaves as keep because the launcher hides connectors only when the value is none (internal/cli/launcher/run.go). The change adds a field and stays in JSON envelope version 1. Helpers: EffectiveConnectors, IsStrict, UnsetDefaults in internal/profile/manifest.go. | High | A new MCP scalar gets a default that differs from unset, or a consumer needs to tell unset from the default in diff |
| D-64 | 2026-10-10 | account add and account rm print a warning when a save drops comments. It goes to stderr after the write and says wrote <path> without its comments, with the backup path. It appears only when the write happens and the file that was read had a comment. account rm --json writes a JSON document of kind account-rm. It has name, config_dir, was_default, directory_deleted and comments_dropped. account add --json also has comments_dropped. Errors keep their text and exit codes. | Follow-up to D-62. The save in D-62 dropped comments without a word, and account rm ignored --json. Code: writableConfig.warnCommentsDropped in internal/cli/launcher/session.go. Tests: go test ./internal/cli/launcher -run Account. | High | Comment-preserving TOML edit replaces the re-encoding |
| D-65 | 2026-10-14 | run --install-missing installs the plugins that a profile lists and claude does not. Amends the "never install silently" rule of launcher.md and workflows.md: it is opt-in, never silent, and never a default or a config key. A bare flag installs all missing plugins. --install-missing=id,id installs only the named plugins, each of which must be in plugins.include. In a terminal, ccshelf asks first (default no). --yes skips the question, also for a bare flag. Without a terminal and without --yes, it exits 2. Accepted risk (user decision): a bare flag with --yes installs plugins that no person read at that time. The trust check of the profile closure is the only gate, and it never meant consent to install code. Each command is printed first. It installs only from a marketplace that is already added and listed once (never npm, never --marketplace, never -y), lists and installs from the cache directory so a project cannot shadow a marketplace (SR3), refuses a profile that has a project-source file, and runs after the trust check. A refused or failed install is a warning and the run goes on as without the flag. dry-run has no such flag. | The user asked for it. An adversarial review (Opus subagent) found that install ran in the project directory, that a failed install aborted instead of degrading (R3), and that project sources were not guarded. The reviewer also warned that a bare flag with --yes installs unread code. The user chose to allow it. The other findings are fixed in the change. | User; behavior unverified (not compiled or run in the authoring sandbox) | A report of a profile that installed something unexpected, or a Claude Code change to plugin install |