From 7debdd305cee1662636106a1523e56a027e11f44 Mon Sep 17 00:00:00 2001 From: Zachajones Date: Wed, 23 Sep 2026 14:57:38 -0500 Subject: [PATCH] fix(cli): write the macOS keychain secret through `security -i`, not stdin to `-w` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `security add-generic-password … -w` with no value reads the password from the controlling terminal, never from stdin. With stdin piped and no terminal it stores an empty password and exits 0, so `auth login` reported "authenticated" while the saved profile could never be read back and `auth status` reported it invalid. `security -i` reads whole commands from stdin, so the add-generic-password line goes over the pipe with the secret single-quoted, and the secret never appears in argv. Stored values are JSON of base64url/hex tokens and UUID accounts, so a quote or newline never occurs; the helper refuses one rather than guessing at the interactive tokenizer's escaping. Verified on macOS 14 (v0.1.1 portable package): the previous item read back empty; a JSON value with double quotes and a space round-trips exactly through `security -i`. Co-Authored-By: Claude Fable 5.1 --- src/cli/system-credential-store.ts | 31 +++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/cli/system-credential-store.ts b/src/cli/system-credential-store.ts index d0ea8063..364fac79 100644 --- a/src/cli/system-credential-store.ts +++ b/src/cli/system-credential-store.ts @@ -167,24 +167,41 @@ function macOsCredentialStore( label: "artifact-server-cli-profile", })), ), + // `security` takes the `-w` value only from argv or a terminal prompt, + // never from stdin, so the piped secret was stored as an empty password + // and every later read came back blank. `security -i` reads whole + // commands from stdin, which keeps the secret out of argv. write: (account, secret) => runCredentialProcess({ - arguments: [ + arguments: ["-i"], + environment, + executable: "/usr/bin/security", + input: [ "add-generic-password", "-a", - account, + quoteForSecurity(account), "-s", - credentialService, + quoteForSecurity(credentialService), "-U", "-w", - ], - environment, - executable: "/usr/bin/security", - input: `${Redacted.value(secret)}\n`, + quoteForSecurity(Redacted.value(secret)), + ].join(" ") + "\n", operation: "write", }).pipe(Effect.asVoid), }; } +/** + * Quote one argument for `security -i`. Stored values are JSON of base64url + * or hex tokens and UUID accounts, so a single quote or newline never occurs; + * refuse rather than guess at the interactive tokenizer's escaping. + */ +function quoteForSecurity(value: string): string { + if (value.includes("'") || value.includes("\n")) { + throw new Error("A credential value cannot contain a single quote or newline."); + } + return `'${value}'`; +} + function linuxCredentialStore( environment: NodeJS.ProcessEnv, ): CliCredentialStoreOperations {