Closes#83. Follow-up to #76/#82: BCC warns when a config holds a raw credential and points at ${VAR}, but gave no way to make the change. This adds the one-click conversion — right-click a secret row in the env or headers table.
The value is about to leave the file, so the action's real job is handing the secret back before it does.
move_value_to_env_ref — data in → newdata out; replaces one env/header/args value with ${VAR} and returns the removed secret; never mutates the input; returns None if the target is missing, non-string, or already a reference.
can_move_value_to_env_ref — the gate: offer only a real stored secret, not already a ref, and only on a client that expands references. Offering it on Claude Desktop would author a config that reaches the server as literal ${VAR} text — the exact failure #76 exists to prevent — so a non-expanding profile refuses outright.
is_env_var_set — skip the copy-the-secret ceremony when the variable already looks set.
GUI:
KeyValueTable gains a context menu gated on can_move_value_to_env_ref, so the action never appears on a non-secret row or a Claude Desktop profile.
MoveToEnvDialog lets the user name the variable (defaulting to the sanitized key), shows the platform-appropriate shell line live, notes when the variable already looks set, and on accept copies the secret to the clipboard before the cell is replaced with the reference.
Wired through ServerEditor.set_profile_provider so the tables know which client the loaded profile targets.
Scope note: env and headers rows for now. The core already handles args by index; wiring the args editor (a free-text widget, not a table) is a small follow-up, deliberately not bundled here.
Verification limits: the GUI is untestable in CI (no PySide6). All decision logic lives in bcc_core and is tested there; the dialog/context-menu/clipboard wiring is worth a human click-through — right-click a secret env value on a Claude Code profile, confirm the shell line is right and the cell becomes ${VAR}, and confirm the action does not appear on a Claude Desktop profile.
Closes #83. Follow-up to #76/#82: BCC warns when a config holds a raw credential and points at `${VAR}`, but gave no way to *make* the change. This adds the one-click conversion — right-click a secret row in the env or headers table.
The value is about to leave the file, so the action's real job is **handing the secret back before it does**.
**Core (pure, fully tested):**
- `sanitize_env_var_name` — key → legal upper-case shell name (`api-key` → `API_KEY`, `2fa` → `_2FA`, non-ASCII/empty handled).
- `shell_export_lines` — the exact `export` / `setx` line, POSIX single-quoted safely (handles embedded `'`).
- `move_value_to_env_ref` — `data` in → **new** `data` out; replaces one env/header/args value with `${VAR}` and returns the removed secret; never mutates the input; returns `None` if the target is missing, non-string, or already a reference.
- `can_move_value_to_env_ref` — the gate: offer only a real stored secret, not already a ref, **and only on a client that expands references**. Offering it on Claude Desktop would author a config that reaches the server as literal `${VAR}` text — the exact failure #76 exists to prevent — so a non-expanding profile refuses outright.
- `is_env_var_set` — skip the copy-the-secret ceremony when the variable already looks set.
**GUI:**
- `KeyValueTable` gains a context menu gated on `can_move_value_to_env_ref`, so the action never appears on a non-secret row or a Claude Desktop profile.
- `MoveToEnvDialog` lets the user name the variable (defaulting to the sanitized key), shows the platform-appropriate shell line live, notes when the variable already looks set, and on accept copies the secret to the clipboard before the cell is replaced with the reference.
- Wired through `ServerEditor.set_profile_provider` so the tables know which client the loaded profile targets.
**Scope note:** env and headers rows for now. The core already handles **args** by index; wiring the args editor (a free-text widget, not a table) is a small follow-up, deliberately not bundled here.
**Tests:** +15 core (name sanitization incl. non-ASCII/leading-digit/empty, POSIX quote safety, the gate across secret / non-secret / already-ref / non-expanding-client, env+headers+args rewrite, input-not-mutated, missing/non-string/already-ref → `None`, `is_env_var_set`). **473 passed**, ruff clean.
**Verification limits:** the GUI is untestable in CI (no PySide6). All decision logic lives in `bcc_core` and is tested there; the dialog/context-menu/clipboard wiring is worth a human click-through — right-click a secret env value on a Claude Code profile, confirm the shell line is right and the cell becomes `${VAR}`, and confirm the action does **not** appear on a Claude Desktop profile.
Follow-up to #76/#82: BCC warns when a config holds a raw credential and
points at ${VAR}, but gave no way to make the change. This adds the
one-click conversion, right-click a secret row in the env or headers table.
The value is about to leave the file, so the action's real job is handing
the secret back before it does:
- Core (pure, tested): sanitize_env_var_name (key -> legal upper-case shell
name; 'api-key' -> API_KEY, '2fa' -> _2FA, non-ASCII/empty handled),
shell_export_lines (the exact export/setx line, POSIX single-quoted
safely), move_value_to_env_ref (data in -> new data out, replaces one
env/header/args value with ${VAR}, returns the removed secret; never
mutates the input; None if the target is missing, non-string, or already a
reference), can_move_value_to_env_ref (offer only a real stored secret, not
already a ref, AND only on a client that expands references -- offering it
on Claude Desktop would author a config that reaches the server as literal
${VAR}, the exact failure #76 exists to prevent), and is_env_var_set (skip
the ceremony when the variable already looks set).
- GUI: KeyValueTable gains a context menu gated on can_move_value_to_env_ref
(so it never appears on a non-secret row or a Claude Desktop profile).
MoveToEnvDialog lets the user name the variable (defaulting to the
sanitised key), shows the platform-appropriate shell line live, notes when
the variable already looks set, and on accept copies the secret to the
clipboard before the cell is replaced with the reference. Wired through
ServerEditor.set_profile_provider so the tables know which client is loaded.
Scope note: env and headers rows for now. The core already handles args by
index; wiring the args editor (a free-text widget, not a table) is a small
follow-up, deliberately not bundled here.
Tests: +15 core (name sanitisation incl. non-ASCII/leading-digit/empty,
POSIX quote safety, the gate across secret/non-secret/already-ref/
non-expanding-client, env+headers+args rewrite, input-not-mutated,
missing/non-string/already-ref -> None, is_env_var_set). 478 passed, ruff
clean. GUI is untestable in CI (no PySide6); the decision logic all lives in
bcc_core and is tested there.
Closes#83
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
Paste from clipboard → the original secret (handed back before removal).
No secret-warning on the resulting ${VAR}.
Headers + persistence
Same works on a remote server's Headers table (e.g. Authorization).
Save, open the file on disk → block holds ${VAR}, plaintext secret is gone.
Re-open the profile → still shows ${VAR}.
Undo (nice-to-have)
Ctrl+Z / Cmd+Z restores the previous value.
Known scope (not bugs)
Args rows are out of scope here (core supports them; the args editor is free-text, a deliberate follow-up).
The "already set" check reads BCC's environment, which may differ from the client's — advisory by design.
If a box fails, tell me which and what you saw and I'll fix + re-push. All pass → approve/merge (or tell me to merge).
## Manual test checklist (GUI — the part CI can't cover)
The logic has 15 unit tests in CI; this is only the click-through, ~10 min.
### Setup
```bash
cd ~/Documents/Claude/Projects/BetterClaudeConfig/better-claude-config
git fetch origin && git checkout feat/83-move-to-env-var && git pull
source .venv/bin/activate
python bcc.py
```
Use a **Claude Code** profile with a secret-looking env value, e.g. `env: { "API_KEY": "ghp_test123" }` (throwaway value is fine).
### Gating — where the action appears
- [ ] Right-click the **value cell** of a secret env row on a **Claude Code** profile → **"Move to environment variable…"** appears.
- [ ] Non-secret row (`REGION` = `us-east-1`) → item does **not** appear.
- [ ] Row already holding `${API_KEY}` → item does **not** appear.
- [ ] On a **Claude Desktop** profile, secret row → item does **not** appear. *(The key gate: Desktop doesn't expand `${VAR}`, so offering it would break the config.)*
### Dialog
- [ ] Variable pre-filled from the key, sanitized (`api-key` → `API_KEY`).
- [ ] Editing the name updates the **shell line live**, correct for your platform (`export VAR='…'` / `setx VAR "…"`).
- [ ] A variable already set in your shell → green "already looks set" note; otherwise hidden.
- [ ] **Cancel** → nothing changes.
### Conversion
- [ ] **Move && copy secret** → cell shows `${VAR}` (visible, not dots); window dirty.
- [ ] Paste from clipboard → the **original secret** (handed back before removal).
- [ ] No secret-warning on the resulting `${VAR}`.
### Headers + persistence
- [ ] Same works on a remote server's **Headers** table (e.g. `Authorization`).
- [ ] **Save**, open the file on disk → block holds `${VAR}`, plaintext secret is **gone**.
- [ ] Re-open the profile → still shows `${VAR}`.
### Undo (nice-to-have)
- [ ] Ctrl+Z / Cmd+Z restores the previous value.
### Known scope (not bugs)
- **Args rows** are out of scope here (core supports them; the args editor is free-text, a deliberate follow-up).
- The "already set" check reads **BCC's** environment, which may differ from the client's — advisory by design.
If a box fails, tell me which and what you saw and I'll fix + re-push. All pass → approve/merge (or tell me to merge).
Two things surfaced testing the GUI:
1. Args rows showed the secret warning but no move action -- the args
editor is a free-text widget, not a table, and was deliberately left out
of the first cut. Wired it up: ArgsEdit gains a context menu that offers
"Move to environment variable…" on exactly the args that look like a
credential. New pure core: secret_arg_indices (which args are secrets,
mirroring args_secret_warning per-index) and suggested_env_var_for_arg
(default var name from the preceding flag -- `--api-key <secret>` ->
API_KEY, else SECRET). The move replaces that one arg line with ${VAR}
and copies the secret to the clipboard, same contract as the tables.
2. On a Claude Desktop profile (or any non-expanding client) the menu showed
NOTHING, so it read as broken. Now a real stored secret always shows the
item -- enabled on a client that expands references, or disabled with the
reason ("unavailable for Claude Desktop -- it doesn't expand ${VAR}") so
the gate is visible rather than silent. Applies to env, headers and args.
Not a change: after converting, env_ref_warnings still notes a variable that
isn't set in the environment. That's #82's advisory doing its job -- the user
runs the export line the dialog handed them; auto-adding a ':-default' would
bake a fallback back into the config and defeat moving the secret out.
Tests: +5 core (secret_arg_indices for token/flag-value/embedded-URL/
reference-excluded, suggested_env_var_for_arg with and without a flag).
483 passed, ruff clean. GUI wiring (context menus) remains untestable in CI.
Refs #83
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
Field testing showed the feature was doing the right thing but describing it
wrong, and left the moved variable invisible. Reworked per that feedback:
Naming. "Move to environment variable" read like "move into the Environment
variables table"; it actually creates a ${VAR} reference to the shell/OS
environment. Renamed the action to "Replace with a ${VAR} reference (out of
file)…" everywhere, and the dialog now says plainly the secret goes to your
shell environment -- not this file, not the table below.
Visibility (the real gap). A lone ${SECRET_KEY} with nothing saying whether
it's wired up isn't much better than a mystery. New Variables… button opens
ReferencedVarsDialog: every ${VAR} the loaded server references, each with
✓ set / ✓ default / ✗ not set and the exact export/setx line to set it.
Backed by pure core.referenced_env_vars (dedupes across fields, resolves
against a given environment or a default).
Two actions on an args secret, because the user may want either:
* "Replace with a ${VAR} reference (out of file)…" -- secret leaves the
file (needs a client that expands refs; disabled with reason on Desktop).
* "Move into Environment variables (kept in this config)…" -- relocates the
arg into the env block where it's visible and editable. Works on any
client (no ${VAR} needed). core.move_arg_to_env_block drops the flag+value
and sets env[VAR]; the dialog warns it changes how the server launches.
Both args actions run through ServerEditor (moving into env touches args AND
the env table), which reloads the form from the transformed data.
Tests: +10 core (referenced_env_vars dedupe/status/default; move_arg_to_env_block
flag+value removal, bare positional, None on bad target). 488 passed, ruff
clean. New dialogs/menus are GUI, untestable in CI as before.
Refs #83
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE
the_og
merged commit dc9e035781 into main2026-08-12 02:10:56 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #83. Follow-up to #76/#82: BCC warns when a config holds a raw credential and points at
${VAR}, but gave no way to make the change. This adds the one-click conversion — right-click a secret row in the env or headers table.The value is about to leave the file, so the action's real job is handing the secret back before it does.
Core (pure, fully tested):
sanitize_env_var_name— key → legal upper-case shell name (api-key→API_KEY,2fa→_2FA, non-ASCII/empty handled).shell_export_lines— the exactexport/setxline, POSIX single-quoted safely (handles embedded').move_value_to_env_ref—datain → newdataout; replaces one env/header/args value with${VAR}and returns the removed secret; never mutates the input; returnsNoneif the target is missing, non-string, or already a reference.can_move_value_to_env_ref— the gate: offer only a real stored secret, not already a ref, and only on a client that expands references. Offering it on Claude Desktop would author a config that reaches the server as literal${VAR}text — the exact failure #76 exists to prevent — so a non-expanding profile refuses outright.is_env_var_set— skip the copy-the-secret ceremony when the variable already looks set.GUI:
KeyValueTablegains a context menu gated oncan_move_value_to_env_ref, so the action never appears on a non-secret row or a Claude Desktop profile.MoveToEnvDialoglets the user name the variable (defaulting to the sanitized key), shows the platform-appropriate shell line live, notes when the variable already looks set, and on accept copies the secret to the clipboard before the cell is replaced with the reference.ServerEditor.set_profile_providerso the tables know which client the loaded profile targets.Scope note: env and headers rows for now. The core already handles args by index; wiring the args editor (a free-text widget, not a table) is a small follow-up, deliberately not bundled here.
Tests: +15 core (name sanitization incl. non-ASCII/leading-digit/empty, POSIX quote safety, the gate across secret / non-secret / already-ref / non-expanding-client, env+headers+args rewrite, input-not-mutated, missing/non-string/already-ref →
None,is_env_var_set). 473 passed, ruff clean.Verification limits: the GUI is untestable in CI (no PySide6). All decision logic lives in
bcc_coreand is tested there; the dialog/context-menu/clipboard wiring is worth a human click-through — right-click a secret env value on a Claude Code profile, confirm the shell line is right and the cell becomes${VAR}, and confirm the action does not appear on a Claude Desktop profile.the_og referenced this pull request2026-08-03 23:39:29 -04:00
Follow-up to #76/#82: BCC warns when a config holds a raw credential and points at ${VAR}, but gave no way to make the change. This adds the one-click conversion, right-click a secret row in the env or headers table. The value is about to leave the file, so the action's real job is handing the secret back before it does: - Core (pure, tested): sanitize_env_var_name (key -> legal upper-case shell name; 'api-key' -> API_KEY, '2fa' -> _2FA, non-ASCII/empty handled), shell_export_lines (the exact export/setx line, POSIX single-quoted safely), move_value_to_env_ref (data in -> new data out, replaces one env/header/args value with ${VAR}, returns the removed secret; never mutates the input; None if the target is missing, non-string, or already a reference), can_move_value_to_env_ref (offer only a real stored secret, not already a ref, AND only on a client that expands references -- offering it on Claude Desktop would author a config that reaches the server as literal ${VAR}, the exact failure #76 exists to prevent), and is_env_var_set (skip the ceremony when the variable already looks set). - GUI: KeyValueTable gains a context menu gated on can_move_value_to_env_ref (so it never appears on a non-secret row or a Claude Desktop profile). MoveToEnvDialog lets the user name the variable (defaulting to the sanitised key), shows the platform-appropriate shell line live, notes when the variable already looks set, and on accept copies the secret to the clipboard before the cell is replaced with the reference. Wired through ServerEditor.set_profile_provider so the tables know which client is loaded. Scope note: env and headers rows for now. The core already handles args by index; wiring the args editor (a free-text widget, not a table) is a small follow-up, deliberately not bundled here. Tests: +15 core (name sanitisation incl. non-ASCII/leading-digit/empty, POSIX quote safety, the gate across secret/non-secret/already-ref/ non-expanding-client, env+headers+args rewrite, input-not-mutated, missing/non-string/already-ref -> None, is_env_var_set). 478 passed, ruff clean. GUI is untestable in CI (no PySide6); the decision logic all lives in bcc_core and is tested there. Closes #83 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE10834a343ato8fdbe90b37Manual test checklist (GUI — the part CI can't cover)
The logic has 15 unit tests in CI; this is only the click-through, ~10 min.
Setup
Use a Claude Code profile with a secret-looking env value, e.g.
env: { "API_KEY": "ghp_test123" }(throwaway value is fine).Gating — where the action appears
REGION=us-east-1) → item does not appear.${API_KEY}→ item does not appear.${VAR}, so offering it would break the config.)Dialog
api-key→API_KEY).export VAR='…'/setx VAR "…").Conversion
${VAR}(visible, not dots); window dirty.${VAR}.Headers + persistence
Authorization).${VAR}, plaintext secret is gone.${VAR}.Undo (nice-to-have)
Known scope (not bugs)
If a box fails, tell me which and what you saw and I'll fix + re-push. All pass → approve/merge (or tell me to merge).
Two things surfaced testing the GUI: 1. Args rows showed the secret warning but no move action -- the args editor is a free-text widget, not a table, and was deliberately left out of the first cut. Wired it up: ArgsEdit gains a context menu that offers "Move to environment variable…" on exactly the args that look like a credential. New pure core: secret_arg_indices (which args are secrets, mirroring args_secret_warning per-index) and suggested_env_var_for_arg (default var name from the preceding flag -- `--api-key <secret>` -> API_KEY, else SECRET). The move replaces that one arg line with ${VAR} and copies the secret to the clipboard, same contract as the tables. 2. On a Claude Desktop profile (or any non-expanding client) the menu showed NOTHING, so it read as broken. Now a real stored secret always shows the item -- enabled on a client that expands references, or disabled with the reason ("unavailable for Claude Desktop -- it doesn't expand ${VAR}") so the gate is visible rather than silent. Applies to env, headers and args. Not a change: after converting, env_ref_warnings still notes a variable that isn't set in the environment. That's #82's advisory doing its job -- the user runs the export line the dialog handed them; auto-adding a ':-default' would bake a fallback back into the config and defeat moving the secret out. Tests: +5 core (secret_arg_indices for token/flag-value/embedded-URL/ reference-excluded, suggested_env_var_for_arg with and without a flag). 483 passed, ruff clean. GUI wiring (context menus) remains untestable in CI. Refs #83 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwEField testing showed the feature was doing the right thing but describing it wrong, and left the moved variable invisible. Reworked per that feedback: Naming. "Move to environment variable" read like "move into the Environment variables table"; it actually creates a ${VAR} reference to the shell/OS environment. Renamed the action to "Replace with a ${VAR} reference (out of file)…" everywhere, and the dialog now says plainly the secret goes to your shell environment -- not this file, not the table below. Visibility (the real gap). A lone ${SECRET_KEY} with nothing saying whether it's wired up isn't much better than a mystery. New Variables… button opens ReferencedVarsDialog: every ${VAR} the loaded server references, each with ✓ set / ✓ default / ✗ not set and the exact export/setx line to set it. Backed by pure core.referenced_env_vars (dedupes across fields, resolves against a given environment or a default). Two actions on an args secret, because the user may want either: * "Replace with a ${VAR} reference (out of file)…" -- secret leaves the file (needs a client that expands refs; disabled with reason on Desktop). * "Move into Environment variables (kept in this config)…" -- relocates the arg into the env block where it's visible and editable. Works on any client (no ${VAR} needed). core.move_arg_to_env_block drops the flag+value and sets env[VAR]; the dialog warns it changes how the server launches. Both args actions run through ServerEditor (moving into env touches args AND the env table), which reloads the form from the transformed data. Tests: +10 core (referenced_env_vars dedupe/status/default; move_arg_to_env_block flag+value removal, bare positional, None on bad target). 488 passed, ruff clean. New dialogs/menus are GUI, untestable in CI as before. Refs #83 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKwBecy6N83jnqQmw8ezwE