"Move to environment variable" — convert a plaintext secret to ${VAR} (#83) #87

Merged
the_og merged 3 commits from feat/83-move-to-env-var into main 2026-08-12 02:10:56 -04:00
Owner

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.

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.
the_og added 1 commit 2026-08-03 23:43:38 -04:00
feat: "Move to environment variable" — convert a plaintext secret to ${VAR} (#83)
CI / Lint (ruff) (pull_request) Successful in 14s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 21s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 20s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 21s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 23s
CI / Catalog signature (pull_request) Successful in 17s
8fdbe90b37
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
the_og force-pushed feat/83-move-to-env-var from 10834a343a to 8fdbe90b37 2026-08-03 23:43:38 -04:00 Compare
Author
Owner

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

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).

## 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).
the_og added 1 commit 2026-08-04 00:03:52 -04:00
feat(#83): offer move-to-env on args rows; explain the gate instead of an empty menu
CI / Lint (ruff) (pull_request) Successful in 19s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 24s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 29s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 31s
CI / Catalog signature (pull_request) Successful in 22s
4743c4a995
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
the_og added 1 commit 2026-08-04 00:25:33 -04:00
feat(#83): variables readout, two clear move actions, args->env relocation
CI / Lint (ruff) (pull_request) Successful in 11s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 30s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 19s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 20s
CI / Catalog signature (pull_request) Successful in 13s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 18s
694439b6f3
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 main 2026-08-12 02:10:56 -04:00
the_og deleted branch feat/83-move-to-env-var 2026-08-12 02:10:57 -04:00
Sign in to join this conversation.