diff --git a/docs/capy-install.md b/docs/capy-install.md
index 1477f21e7..8ad45c445 100644
--- a/docs/capy-install.md
+++ b/docs/capy-install.md
@@ -344,9 +344,12 @@ values landed in its tool output. Never `export $(...)`, `env`, `printenv`, a
bare `set` or a bare `export` in a tool fence (`test/launch-env-echo-ratchet.test.ts`
keeps them out of every skill fence and shell helper), and the redaction
engine now refuses the VALUE of any `*_KEY`, `*_TOKEN`, `*_SECRET`,
-`*_PASSWORD` or `*_PASSWD` variable in the writer's environment (any case;
-names carrying a `PUBLIC` word are framework-public and exempt) (`env.value`,
-HIGH), so a leaked value cannot reach a PR body, receipt, report or Drive file.
+`*_PASSWORD`, `*_PASSWD`, `*_PASS`, `*_PAT`, `*_APIKEY` or `*_PWD` variable
+in the writer's environment (any case; names carrying a `PUBLIC` word are
+framework-public and exempt, gstack's own `GSTACK_BRAIN_LOCK_TOKEN` is not a
+secret, and a value that is a filesystem path names where the credential lives
+rather than the credential) (`env.value`, HIGH), so a leaked value cannot reach
+a PR body, receipt, report or Drive file.
`PATH=
:$PATH` and `GSTACK_LAUNCH_PATH` appear only when the certified Bun,
or the project's resolved Node, is not first on PATH (older Capy images ship
diff --git a/lib/redact-engine.ts b/lib/redact-engine.ts
index 702bd2be5..d3e39ece9 100644
--- a/lib/redact-engine.ts
+++ b/lib/redact-engine.ts
@@ -79,8 +79,9 @@ export interface ScanOptions {
sourcePath?: string;
/**
* The environment whose credential-named variables (a name whose last
- * `_`-separated word is KEY, TOKEN, SECRET, PASSWORD or PASSWD, any case,
- * and that carries no PUBLIC word) are scanned for BY VALUE: wherever one of those
+ * `_`-separated word is KEY, TOKEN, SECRET, PASSWORD, PASSWD, PASS, PAT or
+ * APIKEY, or that ends in _PWD, any case, carrying no PUBLIC word; see
+ * `credentialEnvValues`) are scanned for BY VALUE: wherever one of those
* values appears in the text it is an `env.value` HIGH finding, whatever its
* shape, so a value that reached the text (a dumped environment in a tool
* output, GSTA-96) never reaches a PR body, receipt, report or Drive file.
@@ -518,25 +519,31 @@ function scanInternal(input: string, opts: ScanOptions, spans?: Map): Array<{ name: string; value: string }> {
const out: Array<{ name: string; value: string }> = [];
for (const [name, raw] of Object.entries(env)) {
- if (!CREDENTIAL_ENV_NAME.test(name) || PUBLIC_ENV_NAME.test(name)) continue;
+ if (!CREDENTIAL_ENV_NAME.test(name) || PUBLIC_ENV_NAME.test(name) || NON_SECRET_ENV_NAMES.has(name)) continue;
const value = raw?.trim() ?? "";
if (value.length < MIN_ENV_VALUE_LENGTH) continue;
- if (isPlaceholderSpan(value)) continue;
+ if (isPlaceholderSpan(value) || PATH_VALUE.test(value)) continue;
out.push({ name, value });
}
return out;
diff --git a/test/redact-env-value.test.ts b/test/redact-env-value.test.ts
index a27ab7c48..7282f7ecb 100644
--- a/test/redact-env-value.test.ts
+++ b/test/redact-env-value.test.ts
@@ -23,6 +23,10 @@ const dbPasswdValue = "passwd-passwd-passwd";
const lowerCaseValue = "lower-case-name-value";
const monkeyValue = "banana-banana-banana";
const tokenizerValue = "tokenizer-value-here";
+const sslKeyPath = "/etc/ssl/private/x.key";
+const winKeyPath = "C:\\Users\\me\\key.pem";
+const homeKeyPath = "~/.ssh/id_key";
+const lockOwner = ["12345", "6789", "17"].join("-");
const env = {
ANTHROPIC_API_KEY: anthropicValue,
@@ -139,6 +143,23 @@ describe("env-value scan", () => {
expect(credentialEnvValues({ PUBLICITY_KEY: monkeyValue }).map(e => e.name)).toEqual(["PUBLICITY_KEY"]);
});
+ // /ship Step 11 adversarial fixes: (1) the name rule also admits *_PASS, *_PAT, *_APIKEY and *_PWD,
+ // while the shell's own PWD and OLDPWD stay out; (2) a credential-named variable whose value is a
+ // filesystem path (SSL_KEY=/etc/ssl/private/x.key) names where the credential lives, so the path
+ // is not a finding; (3) gstack's own GSTACK_BRAIN_LOCK_TOKEN (pid-random-epoch, written to the lock
+ // owner file) is not a secret. fails_when: PWD is admitted (every mention of the cwd becomes HIGH),
+ // the path rule goes, or the non-secret set loses the lock token.
+ test("more credential suffixes, path values and gstack's own lock token", () => {
+ const names = credentialEnvValues({
+ DB_PASS: dbValue, GITHUB_PAT: ghValue, STRIPE_APIKEY: lowerCaseValue, MYSQL_PWD: dbPasswdValue,
+ PWD: "/workspace/gstack", OLDPWD: "/workspace/other",
+ SSL_KEY: sslKeyPath, WIN_KEY: winKeyPath, HOME_KEY: homeKeyPath,
+ GSTACK_BRAIN_LOCK_TOKEN: lockOwner,
+ }).map(e => e.name).sort();
+ expect(names).toEqual(["DB_PASS", "GITHUB_PAT", "MYSQL_PWD", "STRIPE_APIKEY"]);
+ expect(credentialEnvValues({ SLASH_KEY: "/abc12345-no-second-slash" }).map(e => e.name)).toEqual(["SLASH_KEY"]);
+ });
+
test("the default environment is process.env", () => {
const name = "GSTA104_TEST_TOKEN";
process.env[name] = "v4lue-only-this-test-knows";