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.
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:
| 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.appin 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 below for why, and for the public key value
to use with --pubkey-b64 below.
macOS / Linux
# 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):
python3 scripts/sign_checksums.py verify \
--sums SHA256SUMS --sig SHA256SUMS.sig \
--pubkey-b64 "<the release public key from Signing keys, below>"
Windows (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, 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:
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'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), 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 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
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 (soClaudeandClaude-Workboth show up) and finds Claude Code's user-scope config at~/.claude.json(the fileclaude mcp addwrites). 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
mcpServersblock, 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
.jsonfile 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
commandis 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
.appmay 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— regeneratesicons/app.icnsandicons/app.icofrom source PNGs.scripts/sign_checksums.py— generates and Ed25519-signs the releaseSHA256SUMSmanifest (see Verifying your download).catalog_console.py/catalog_review.py— maintainer-only, never shipped to users (excluded frombcc.spec; seetests/test_catalog_console_packaging.py). The Catalog Console: review + signdata/catalog.json(againstmain, 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 redmain), generate/manage both signing keys (keygen,keygen --release), and report on their status (keys, no passphrase needed) — see Signing keys.
Building from source
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:
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.