Includes the required regression case: rewrite a file with different
content, force the original mtime back via os.utime, and assert the
fingerprint still differs (because size changed).
MainWindow._loaded_mtime -> _loaded_stat (core.ConfigStat), populated
via core.config_fingerprint() at load/save time. The save-path stale
check now compares the full fingerprint (mtime AND size) instead of
bare mtime equality.
Two prior attempts to fix this line via a literal or -escaped
non-breaking space both silently reverted back to a no-op regular-space
replace during transcription. Using chr(0xA0) instead removes any
non-ASCII or backslash-escape character from the source line entirely.
The previous commit accidentally normalized the non-breaking space
(U+00A0) in _normalize_unicode's replace() call to a regular space
during a copy/paste, turning that replace() into a no-op. Use an
explicit escape instead of the literal character so it can't
be silently corrupted again.
Bare mtime equality can miss a concurrent external write that lands
within the filesystem's mtime resolution (same-second writes), or
where the writer restores the original mtime. Add config_fingerprint()
returning a (mtime, size) ConfigStat pair; the stale-file check in
bcc.py now compares both fields instead of mtime alone. config_mtime()
is kept as-is (still used/tested independently).
The crashed/exited/stderr spawn tests used 0.3-0.5s timeouts. On a slow CI
runner, interpreter startup can exceed that, so the process is still starting
when the timeout fires, gets killed, and is misclassified "ok" (still running)
instead of "crashed"/"exited". Bump those three to 2.0s. The two "ok" paths
(sleep-60, stdin-block) stay at 0.3s since timing out there means success.
Adds `args_secret_warning(data)` to bcc_core — returns a warning string
when any arg positional value looks like a raw credential (token prefix,
value following a secret-named flag, or URL with embedded user:pass like
postgres://user:pass@host). `--flag=value` inline forms are intentionally
skipped (the flag name already labels the value).
Adds `secret_warn` QLabel in ServerEditor's stdio page; shown/hidden by
`_check_args()` on every field change, and cleared on deselect or
stdio→remote type switch. Non-blocking — save path is not touched.
Closes#1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Records the file mtime at load time; before write_config() fires, re-checks
it. If it changed (e.g. `claude mcp add`, a second BCC window, or Claude
itself writing ~/.claude.json), StaleDialog prompts with the changed top-level
key names and a masked server-section diff. "Merge & save" applies the user's
in-memory server edits on top of the current on-disk file (preserving external
non-server changes); "Overwrite anyway" proceeds as before.
- bcc_core: config_mtime(), external_change_summary(), _server_sections()
helper extracted from backup_diff for reuse
- bcc.py: StaleDialog, MainWindow._loaded_mtime tracked through load/save
- tests: 7 new tests (config_mtime, external_change_summary variants, AC merge test)
Closes#4
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The diff preview in RestoreDialog was serializing the full config dict,
exposing secret env values and token args in cleartext on a pasteable surface.
- Add _redact_server_data / _redact_servers_block helpers that apply
redact_args to args and mask env values for is_secret_key() keys
- Rewrite backup_diff to compare only {mcpServers, _disabledMcpServers}
sections (sanitized), not the whole file — also avoids double-serializing
multi-MB ~/.claude.json for a servers-only diff
- Add clarifying comment in _restore_from_backup about why full_config
is the right base after confirm-discard
- Add test: backup_diff with secret args/env → MASK in output, raw values absent
- Add test: restore_backup restores _disabledMcpServers correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add list_backups / backup_label / backup_diff / restore_backup to bcc_core,
RestoreDialog to bcc.py, and a "Restore…" button in the profile top bar.
Restore is selective: only mcpServers and _disabledMcpServers are replaced;
all other keys in the config (history, project state) are preserved verbatim.
Goes through write_config() so a pre-restore backup is always created first.
Closes#3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caught in supervisor review:
1. Stderr was silently dropped when the Details panel was closed during a
crash. _on_spawn_done appended to diag_text only when the panel was
already open, and refresh_dependency() clobbered that text on the next
field change anyway.
Fix: stash the result in self._last_spawn; _full_diag_text() appends
the stderr section whenever diag text is generated; _on_spawn_done
auto-opens the panel on non-ok outcomes (same pattern as the existing
auto_open for missing commands).
2. _drain stopped reading once _STDERR_CAP (4 KB) was reached. A process
that writes more than 4 KB then blocked on a full pipe buffer, never
exited, and was misclassified as "ok" instead of "crashed".
Fix: drain to EOF unconditionally; keep only the first _STDERR_CAP bytes.
Regression test: 64 KB stderr + exit(3) → outcome "crashed".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add spawn_test() to bcc_core — spawns a stdio server for up to 3 s,
captures stderr, and reports ok/exited/crashed/not_found. Key design
decisions driven by real MCP server behaviour:
- stdin=PIPE (never written): servers block on JSON-RPC input and stay
alive, so "still running after timeout" reliably signals a healthy
start. stdin=DEVNULL would send EOF, causing well-behaved servers to
exit 0 and be misclassified as "exited".
- Command resolved via shutil.which(augmented_path()) before Popen so
subprocess PATH resolution is unambiguous across platforms.
- start_new_session=True on POSIX + os.killpg on timeout: kills the
whole process group, not just the launcher (npx, uvx), which would
otherwise orphan the actual node/python grandchild process.
- stdout=DEVNULL: draining a PIPE we don't read would deadlock at ~64 KB.
- stderr drained in a daemon thread, capped at 4 KB.
GUI: SpawnTester(QThread) wraps spawn_test; "Test launch" button in
ServerEditor dep row (stdio only, visible when command resolves ok/warn).
Result colours match the existing dep-status palette (green/amber/red).
Stderr appended to the diagnostics panel if it is open.
7 new unit tests cover all outcomes and the stdin-open regression guard.
Ran python bcc.py locally: button appears for stdio servers whose command
resolves, is hidden for remote servers and missing-command servers.
Closes#2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Env/header values whose key looks secret (TOKEN, API_KEY, PASSWORD,
AUTH, ...) render as •••••••• via a display-only delegate; a
'Show secrets' toggle reveals them. Underlying data, editing, and
save are untouched.
- The Add dialog switches the value field to password echo when the
key name looks secret.
- Copy-diagnostics now redacts secrets from args (--token <v>,
--api-key=<v>, and well-known token prefixes like ghp_/sk-/xoxb-),
since those reports get pasted into public bug reports. Env and
header values were already omitted from diagnostics.
Claude Code stores user-scope MCP servers in ~/.claude.json (what
'claude mcp add' writes); ~/.claude/settings.json is for permissions
and hooks and rejects an mcpServers key with a schema error, so BCC
was reading (and writing) servers where Claude Code never looks.
If servers are found parked in settings.json, that file is still
listed as 'Claude Code (legacy settings.json)' so they can be copied
into the real config via Copy to. Docs updated; verified against
docs.claude.com/en/docs/claude-code/settings.
'-apple-system' is a web convention, not a real font family — Qt scans
every installed font trying to resolve it (the 'Populating font family
aliases' warning at startup). Qt already defaults to the native system
UI font on each platform, so don't name UI fonts at all. The diag
panel's 'SF Mono' (not system-installed on macOS) becomes Menlo.
- Label now explains the model with an example: a flag and its value go
on separate lines. Placeholder shows the common uv pattern.
- split_suspicious_args() flags lines that contain whitespace plus a
dash-prefixed token ('--directory /path') — legit single args with
spaces ('My Documents') are never touched. Quotes are respected.
- The editor shows a warning under the args box with a 'Fix: split onto
separate lines' button; the fix goes through the undo stack.
If a config file on disk fails strict JSON parsing, BCC now runs it
through the same repair pipeline as pasted snippets and shows a dialog
listing the parse error, each fix it would apply, and a preview of the
resulting file. The user chooses: Repair & load (marks the profile
dirty; the file is only rewritten on Save, after the broken original
is backed up) or Cancel. Unsalvageable files keep the old error path.
repair_config_file() in bcc_core never writes to disk itself.
The one-arg-per-line model read as odd text wrapping — a path on its
own line looked like a wrapped continuation of the previous argument.
A line-number gutter makes each argument visibly its own item.
ArgsEdit also owns the NoWrap setting now.
- Cell editors in env/header tables got the global QLineEdit style
(6px vertical padding, 7px radius) crammed into a short row, clipping
the text to an unreadable sliver. Give table editors a compact flat
style and bump default row height to 34px.
- Editor select-all now uses dim-orange/white selection colors inside
cells for contrast against the dark field.
- Arguments box no longer soft-wraps: one arg per line means a wrapped
path looks like two args. Long lines scroll horizontally instead.
Editing a table cell select-alls its text; without an explicit
selection-color the highlighted text rendered near-invisible against
the accent-orange selection background. Applies to all line edits,
text areas, and combo boxes.
- Horizontal splitter between the server list and the editor panel
- Vertical splitter between the Active and Disabled tables (drops the
fixed 170px cap on the disabled list)
- Editor fields (args, env/header tables) now grow with the window
instead of being pinned to fixed max heights
- Splitter positions and window geometry persist across launches via
QSettings; handles highlight on hover/drag
Pasted config snippets no longer have to be valid JSON. repair_json_text()
auto-fixes markdown fences, surrounding prose, // /* */ # comments,
trailing and missing commas, smart quotes, single quotes, unquoted keys,
Python/JS literals, and unclosed braces. parse_pasted_json_verbose()
reports every repair applied; the paste dialog now parses as you type
and previews exactly what will be added and what was fixed.
Also includes ruff lint fixes and formatting across bcc.py/bcc_core.py.