Files
better-claude-config/README.md
T
BCC Agent aa40f8e139
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 12s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 25s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 11s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 11s
CI / Catalog signature (pull_request) Failing after 6s
feat(catalog-console): a keys status command, and complete rotation without a red main (#62, #68)
Two problems from issue #68's follow-up review:

1. The maintainer -- the only person who will ever use this tool -- cannot
   reliably tell which of the two signing keys is which or what state
   either is in. He already pasted a private key into a chat window
   because a prompt was ambiguous. That's a defect in this tool, not user
   error.

2. PR #71 rotates bcc_core.CATALOG_PUBKEYS, which makes
   data/catalog.json.sig (signed by the retired key) stop verifying and
   the CI catalog-signature job go red. The Console could previously only
   load/sign against `main`, so the only way through was to merge a red
   PR and fix main afterwards -- normalizing exactly the alarm fatigue
   this whole design exists to prevent.

Task 1 -- `python catalog_console.py keys`:
  A plain-English-first status report for BOTH keys: purpose, where the
  private half lives, whether it exists locally, its fingerprint, whether
  that fingerprint matches every place its public half is expected to be
  committed (bcc_core.CATALOG_PUBKEYS, ci.yml's trust anchor, and
  scripts/sign_checksums.RELEASE_PUBKEYS -- checked independently, since
  issue #68 finding 4 was exactly bcc_core.py and ci.yml silently
  drifting apart), and whether data/catalog.json.sig currently verifies --
  ending with the exact command to run next. Needs no passphrase and never
  touches private key bytes: a plaintext public-key cache
  (store_public_key/load_public_key) is written alongside the existing
  encrypted private blob at keygen time, precisely so this command can
  report a fingerprint without decrypting anything.

  The status/report logic (key_status, render_key_status_report,
  recommend_next_steps, fingerprint_pubkey, extract_pubkey_list_literal,
  extract_ci_trust_anchor_pubkey) is pure and lives in catalog_review.py;
  cmd_keys in catalog_console.py is a thin printer over it, per the
  project's existing pure-core/thin-GUI split.

Task 2 -- rotation completable without a red main:
  ReviewWindow now offers a "current branch" source (auto-detected via
  `current_branch()`, or --ref to name one explicitly) alongside "main"
  and open PRs. Loading it runs the exact same diff-against-last-signed /
  rotation-detection logic "main" always used (_load_own_ref, extracted
  from the old hardcoded-to-main _on_load), just parameterized on the
  ref. Signing now pushes to session.loaded_ref, never a hardcoded "main"
  (commit_and_push_signed_catalog's branch param was already there --
  only the call site was wrong). The ref-list computation itself is a
  pure function (compute_own_refs) so this seam is unit-testable without
  git or Qt. None of can_sign()'s guards (empty-diff, acknowledge-all,
  blocking-risk, TOCTOU) were touched.

  This lets a rotation branch be reviewed, re-attested (every entry,
  since the new key never vouched for any of them -- issue #68 finding 5
  follow-up), signed, and pushed to ITS OWN branch before it's ever
  merged.

Task 3 -- label the keys everywhere:
  PassphraseDialog now shows which key (CATALOG vs RELEASE) and its
  fingerprint before the passphrase field, both in its window title and
  its prompt text -- the exact ambiguity that led to a private key being
  pasted into a chat window. cmd_keygen's stored-key confirmation now
  reads "CATALOG private key encrypted..." / "RELEASE private key
  encrypted..." instead of a capitalized-lowercase kind. The reattest
  banner now says "CATALOG signing key" / "CATALOG key" throughout
  instead of "the key".

PySide6's import is now guarded (try/except -> _PYSIDE6_AVAILABLE) and
every GUI class definition that depends on it moved under
`if _PYSIDE6_AVAILABLE:`. `keygen`, `show-seed-b64`, and the new `keys`
command have no GUI dependency and now work (and are testable) in an
environment without PySide6 -- which is exactly this repo's own `test`
CI job (pytest + cryptography only, no PySide6). `gui` fails with a clear
message instead of an ImportError stack trace if it's missing.

Tests: 26 new pure-function tests in tests/test_catalog_review.py
(fingerprint_pubkey, extract_pubkey_list_literal,
extract_ci_trust_anchor_pubkey, key_status, recommend_next_steps,
render_key_status_report) and a new tests/test_catalog_console_git.py
(14 tests) covering compute_own_refs, current_branch,
commit_and_push_signed_catalog's branch targeting, and
catalog_sig_status_on_disk against real local git repos -- importing
catalog_console.py directly, proving it works without PySide6. 400
passed, 1 skipped (pre-existing). ruff check / ruff format --check clean.
2026-07-13 13:10:36 -04:00

251 lines
12 KiB
Markdown

# Better Claude Config (BCC)
A small, cross-platform GUI for editing the `mcpServers` block of Claude Desktop
and Claude Code installs — without ever hand-writing JSON.
BCC only ever touches `mcpServers` (and its own `_disabledMcpServers` parking
key). Every other key in your config is preserved verbatim, in its original
order. Each write is atomic and makes a timestamped backup first.
## Download (no Python required)
Pre-built self-contained binaries are attached to every [GitHub Release](../../releases):
| Platform | Download | Run |
|----------|----------|-----|
| macOS | `BetterClaudeConfig-macOS.zip` | Unzip → drag **BetterClaudeConfig.app** to Applications |
| Windows | `BetterClaudeConfig-Windows.zip` | Unzip → double-click **BetterClaudeConfig.exe** |
| Linux | `BetterClaudeConfig-Linux.tar.gz` | Extract → run **BetterClaudeConfig** |
> **macOS Gatekeeper note:** the app is not notarized. On first launch, right-click → **Open**, or run `xattr -cr /Applications/BetterClaudeConfig.app` in a terminal.
## Verifying your download
BCC isn't code-signed — there's no budget for a paid certificate (macOS
Developer ID, Windows Authenticode). Instead, every release publishes a
`SHA256SUMS` file listing the checksum of each archive, detached-signed with
Ed25519 as `SHA256SUMS.sig`. Both are attached to the release alongside the
binaries.
**What this proves:** the file you downloaded is byte-for-byte what we
published, and the manifest itself was signed by our release key.
**What this does NOT do:** it does not make the binary "safe," and it does
**not** remove the macOS Gatekeeper or Windows SmartScreen warning — those
are only suppressed by a paid OS-vendor certificate, which this project
doesn't have. Verifying checksums is about detecting tampering in transit or
on a mirror, not about vouching for the software.
This manifest is signed with BCC's **release key**, which is a different
key from the one that signs the MCP server catalog — see
[Signing keys](#signing-keys) below for why, and for the public key value
to use with `--pubkey-b64` below.
### macOS / Linux
```bash
# From inside the folder you downloaded the release files into:
sha256sum -c SHA256SUMS
```
If your `sha256sum` complains about missing files, download `SHA256SUMS`
into the same directory as the archive you downloaded — it lists every
platform's archive, and only the one(s) present will be checked.
To also verify the manifest's signature (optional, requires Python +
`pip install cryptography` and a checkout of this repo):
```bash
python3 scripts/sign_checksums.py verify \
--sums SHA256SUMS --sig SHA256SUMS.sig \
--pubkey-b64 "<the release public key from Signing keys, below>"
```
### Windows (PowerShell)
```powershell
Get-FileHash .\BetterClaudeConfig-Windows.zip -Algorithm SHA256
```
Compare the printed hash (case-insensitively) against the matching line in
`SHA256SUMS`.
### If a release has no `SHA256SUMS.sig`
The signing key is a repo secret that has to be configured manually; if a
release is missing the `.sig` file, the checksums themselves are still
valid and safe to check against — the release workflow only skips signing,
never checksum generation.
## Signing keys
BCC uses **two separate Ed25519 keypairs**, deliberately never the same
key, because they protect different things and live in different places:
| | Catalog key | Release key |
|---|---|---|
| Signs | `data/catalog.json` (the MCP server catalog every user's app trusts) | `SHA256SUMS` (the checksum manifest for release binaries) |
| Verified by | `bcc_core.CATALOG_PUBKEYS` | `scripts/sign_checksums.RELEASE_PUBKEYS` |
| Lives | Offline, passphrase-encrypted, maintainer's machine only (OS keychain or an encrypted file outside the repo — see the [Catalog Console](#files), issue #62) | A Gitea Actions repo secret, `RELEASE_SIGNING_KEY`**intentionally CI-resident** |
| Generated with | `python catalog_console.py keygen` | `python catalog_console.py keygen --release` |
| Exported for CI with | *(never — there is no supported way to export this key)* | `python catalog_console.py show-seed-b64 --release` |
**Confused about which key is which, or what state either is in?** Run:
```bash
python catalog_console.py keys
```
It needs no passphrase (it never touches private key bytes) and prints a
plain-English report for both keys: where each private half lives, whether
it's present on this machine, its fingerprint, whether that fingerprint
matches what's actually committed in `bcc_core.py`, `ci.yml`'s trust
anchor, and `scripts/sign_checksums.py`, and whether
`data/catalog.json.sig` currently verifies — ending with the exact command
to run next for whatever state it finds. This is the check that would have
caught [issue #68](../../issues/68)'s finding 5 incident before it happened.
**Why two keys:** the catalog key is the root of trust for what BCC
actually *executes* on a user's machine — every `command`/`args` pair in
the shipped catalog is only there because this key signed it. If that key
and the release-checksum key were the same (as they briefly were — see
[issue #68](../../issues/68)), then anything that can exfiltrate a Gitea
Actions secret (a malicious workflow-file PR, a compromised runner, a leaky
log) could sign a catalog every user's copy of BCC would trust, not just a
checksum manifest. Splitting them means **a CI/secret compromise burns the
release key, never the catalog key** — checksums for a future release could
be forged, which is bad, but no attacker gains the ability to make BCC run
arbitrary commands on installs that trust the catalog. That asymmetry is
the entire point of having two keys instead of one.
The catalog key is **never** meant to leave the maintainer's machine: it's
generated, stored, unlocked, and used to sign entirely inside the Catalog
Console (`catalog_console.py`), and `catalog_console.py show-seed-b64`
refuses to run without `--release` specifically so the catalog seed can't
be exported by habit or muscle memory.
**Release signing public key** (Ed25519, base64, raw 32 bytes) — this is
the RELEASE key, which signs `SHA256SUMS` (release checksums). It does
**not** sign `data/catalog.json` and is not the key `bcc_core.CATALOG_PUBKEYS`
trusts:
```
6BnPgJEHJFyVltFoLTCNadIsehjy00iiW8IRlC1TfhA=
```
The catalog public key (Ed25519, base64, raw 32 bytes) — this is the key
that signs `data/catalog.json` and is trusted via `bcc_core.CATALOG_PUBKEYS`
and the CI trust anchor in `.github/workflows/ci.yml`. It is listed here
for completeness, not because you need it to verify a download — use the
*release* key above for that:
```
0s24PmkZcTT5yxNDdyTPHl5fyxArrHNPJKBjnXoQd8k=
```
Both keys above were rotated 2026-07 — see [issue #68](../../issues/68)
finding 5. The prior (shared) key is retired and is deliberately **not**
kept in either trust list; retaining a burned key would defeat the point
of rotating it.
## Run from source
```bash
pip install -r requirements.txt
python bcc.py
```
(Python 3.10+, PySide6 6.6+.)
## What it does
- **Auto-discovers installs** — scans the platform's app-support folder for any
`Claude*` directory (so `Claude` and `Claude-Work` both show up) **and** finds
Claude Code's user-scope config at `~/.claude.json` (the file `claude mcp add`
writes). Use **Add config…** to point at any other file manually — e.g. a
project's `.mcp.json`.
- **Form-based editing** — name, command, args (one per line), env vars, or for
remote servers: URL, transport, and headers. No raw JSON.
- **Paste JSON — even broken JSON** — drop in any snippet from an MCP doc (full
`mcpServers` block, inner map, or a single bare server object); it's parsed
and merged. The paste box parses as you type and auto-repairs the stuff docs
and chat windows love to break: markdown fences, surrounding prose, comments,
trailing or missing commas, smart quotes, single quotes, unquoted keys, and
unclosed braces — and tells you exactly what it fixed before you commit.
- **Drag & drop** a `.json` file onto the window to import servers from it.
- **Copy to ▸** — copy the selected server straight into your *other* install.
- **Active / Disabled sections** — servers are shown in two labelled lists with
live counts, so a server that's switched off in the config is obvious at a
glance. Toggle a server's checkbox to move it between sections (disabled
servers are parked under `_disabledMcpServers`, which Claude ignores).
- **Dependency check + diagnostics** — each server shows whether its `command`
is actually found on PATH:
- ● green — found on the normal PATH; will work anywhere.
- ▲ amber (**PATH-risk**) — found, but *only* in a location BCC added on top of
the inherited PATH. It'll work when you launch Claude from a terminal, but a
double-clicked `.app` may not see it. This is the usual cause of "the tool
says it's fine but Claude won't start the server." One click on **Use full
path ↳** rewrites the bare command (`npx`) to its absolute path so any launch
finds it.
- ● red (**broken**) — not found anywhere.
- ◆ blue — remote (url) server. **Test connection** does a live reachability
check on a background thread (any HTTP response = reachable).
Each section header shows a **broken / PATH-risk count badge**, and selecting a
flagged server auto-opens a **Details** panel with a full, copy-pasteable
report: which command resolved to what, tailored install hints, and the exact
PATH that was searched (with `+` marking the dirs BCC added). **Re-check**
re-scans PATH after you install something; **Copy diagnostics** grabs the whole
report for a bug report.
After saving, **restart that Claude install** for changes to take effect.
## Config locations it scans
**Claude Desktop**
| OS | Base it scans for `Claude*` |
|---------|----------------------------------|
| macOS | `~/Library/Application Support` |
| Windows | `%APPDATA%` |
| Linux | `~/.config` |
**Claude Code** (all platforms): `~/.claude.json` (user scope). If servers are
found parked in `~/.claude/settings.json` — where Claude Code ignores them — that
file is also listed, marked *legacy*, so you can copy them over.
## Files
- `bcc.py` — the GUI.
- `bcc_core.py` — all the file/JSON/validation/dependency logic (no GUI deps).
- `test_core.py` — unit suite for the core (`python test_core.py`).
- `bcc.spec` — PyInstaller build spec (cross-platform).
- `scripts/build_icons.py` — regenerates `icons/app.icns` and `icons/app.ico` from source PNGs.
- `scripts/sign_checksums.py` — generates and Ed25519-signs the release `SHA256SUMS` manifest (see [Verifying your download](#verifying-your-download)).
- `catalog_console.py` / `catalog_review.py`**maintainer-only**, never shipped to users (excluded from `bcc.spec`; see `tests/test_catalog_console_packaging.py`). The Catalog Console: review + sign `data/catalog.json` (against `main`, an open PR, or the branch you have checked out — `--ref <branch>` to be explicit, e.g. mid key-rotation, so a rotation can be signed and pushed to its own branch *before* it's merged, never forcing a red `main`), generate/manage both signing keys (`keygen`, `keygen --release`), and report on their status (`keys`, no passphrase needed) — see [Signing keys](#signing-keys).
## Building from source
```bash
pip install -r requirements-dev.txt
python scripts/build_icons.py # regenerate icons if needed
pyinstaller bcc.spec
# macOS → dist/BetterClaudeConfig.app
# Windows → dist/BetterClaudeConfig.exe
# Linux → dist/BetterClaudeConfig
```
Releases are built automatically by GitHub Actions (`.github/workflows/release.yml`) when a version tag is pushed:
```bash
git tag v1.0.0 && git push --tags
```
## Rebrand
The accent color is one constant (`ACCENT`) at the top of `bcc.py`.
## License
MIT — see [LICENSE](LICENSE).