From 9b2f4fe79f08a1ff490dab24643a827b15fae0a7 Mon Sep 17 00:00:00 2001
From: Josh Knapp
Date: Tue, 11 Aug 2026 15:27:19 -0700
Subject: [PATCH] Give the env var its value box back, and stop labelling the
secret
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two separate faults, both reachable from one screenshot of the Global
Environment Variables editor.
The value input was collapsed to a sliver, so a variable looked like it
had lost its value. `inputClass` carries `w-full`, and the `w-2/5` on the
key input did not beat it — class-attribute order is not what resolves
that conflict, stylesheet order is. The key therefore asked for the whole
row, and the value input, whose `flex-1` gives it a basis of 0 and only
the leftover space, got almost nothing. Widths now live on wrapper divs,
where nothing competes with them.
The fingerprint that detects custom-env changes was a plaintext
`KEY=VALUE` join, and it is written as the `triple-c.custom-env-fingerprint`
label. Labels are readable by anything on the host via `docker inspect`,
`docker commit` copies them onto the project's snapshot image, and the
recreation check logs both sides on a mismatch — so an API token set as a
custom variable was published to all three. It is hashed now, exactly as
`triple-c.git-token-hash` already was. Empty stays empty, so "nothing
configured" still reads as an empty label.
Changing the fingerprint format means every project's label mismatches
once: expect a single container recreation per project on next start.
Co-Authored-By: Claude Opus 5 (1M context)
---
app/src-tauri/src/docker/container.rs | 43 +++++++++++++++--
app/src/components/projects/EnvVarsEditor.tsx | 46 +++++++++++--------
2 files changed, 68 insertions(+), 21 deletions(-)
diff --git a/app/src-tauri/src/docker/container.rs b/app/src-tauri/src/docker/container.rs
index 2674cf9..f6698f0 100644
--- a/app/src-tauri/src/docker/container.rs
+++ b/app/src-tauri/src/docker/container.rs
@@ -318,8 +318,19 @@ fn is_reserved_env_key(key: &str) -> bool {
|| RESERVED_ENV_EXACT.iter().any(|e| upper == *e)
}
-/// Compute a fingerprint string for the custom environment variables.
-/// Sorted alphabetically so order changes do not cause spurious recreation.
+/// Compute a fingerprint for the custom environment variables.
+///
+/// Sorted alphabetically so order changes do not cause spurious recreation, and
+/// **hashed**, because this value is written as the
+/// `triple-c.custom-env-fingerprint` label. Labels are readable by anything on
+/// the host through `docker inspect`, `docker commit` copies them onto the
+/// project's snapshot image, and `container_needs_recreation` logs both sides on
+/// a mismatch — so a plaintext `KEY=VALUE` join published every custom
+/// variable's *value*, API tokens included, to all three places. Same treatment
+/// as `triple-c.git-token-hash`.
+///
+/// Empty stays empty rather than becoming the hash of the empty string: an empty
+/// label is how every other `triple-c.*` key says "nothing configured".
fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
let mut parts: Vec = Vec::new();
for env_var in custom_env_vars {
@@ -330,7 +341,10 @@ fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
parts.push(format!("{}={}", key, env_var.value));
}
parts.sort();
- parts.join(",")
+ if parts.is_empty() {
+ return String::new();
+ }
+ sha256_hex(&parts.join(","))
}
/// The shared Claude Code OAuth token to inject for this project, paired with
@@ -2473,6 +2487,29 @@ mod tests {
assert_eq!(fp, "");
}
+ #[test]
+ fn the_custom_env_fingerprint_never_carries_the_value() {
+ // It goes into `triple-c.custom-env-fingerprint`, which `docker inspect`
+ // hands to anything on the host, `docker commit` copies onto the
+ // project's snapshot image, and the recreation check logs on a mismatch.
+ let secret = "33da01c1b320644920c20d6b5e0a1c6b3c3451c2";
+ let fp = compute_env_fingerprint(&[EnvVar {
+ key: "TEA_TOKEN".to_string(),
+ value: secret.to_string(),
+ }]);
+ assert!(!fp.contains(secret), "fingerprint leaked the value: {}", fp);
+ assert!(!fp.contains("TEA_TOKEN"), "fingerprint leaked the key: {}", fp);
+ assert_eq!(fp.len(), 64, "expected a sha256 hex digest, got {:?}", fp);
+
+ // It still has to move when the value does, or a rotated token would
+ // never reach the container.
+ let rotated = compute_env_fingerprint(&[EnvVar {
+ key: "TEA_TOKEN".to_string(),
+ value: "rotated".to_string(),
+ }]);
+ assert_ne!(fp, rotated);
+ }
+
#[test]
fn the_deprecated_small_fast_model_var_is_never_emitted() {
let rendered: Vec = aliases(Some("m"), Some("h"))
diff --git a/app/src/components/projects/EnvVarsEditor.tsx b/app/src/components/projects/EnvVarsEditor.tsx
index 1266283..ce6dde5 100644
--- a/app/src/components/projects/EnvVarsEditor.tsx
+++ b/app/src/components/projects/EnvVarsEditor.tsx
@@ -43,26 +43,36 @@ export default function EnvVarsEditor({
)}
+ {/* The row's widths live on wrapper divs, not on the inputs. `inputClass`
+ carries `w-full`, and a width utility on the input itself does not beat
+ it — class-attribute order is not what resolves the conflict, stylesheet
+ order is. Sizing the key input directly left it asking for the whole row
+ and collapsed the value input, whose `flex-1` basis of 0 gave it only the
+ leftover space, to an unusable sliver. */}
{vars.map((ev, i) => (