Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cc-switch.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const {
ensureDir,
readJson,
readJsonIfExists,
readCredentials,
deepCopy,
writeLiveState,
writeStore,
Expand Down Expand Up @@ -184,7 +185,7 @@ async function main() {

try {
const config = readJson(options.configPath);
const credentials = readJson(options.credentialsPath);
const credentials = readCredentials(options.credentialsPath);
const existingStore = normalizeStore(readJsonIfExists(options.storePath, { version: STORE_VERSION, accounts: [] }), STORE_VERSION);

if (options.usageOnly) {
Expand Down
78 changes: 76 additions & 2 deletions lib/store/io.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');

const KEYCHAIN_SERVICE = 'Claude Code-credentials';
const isDarwin = process.platform === 'darwin';

function getDefaultConfigPath() {
return path.join(os.homedir(), '.claude.json');
Expand Down Expand Up @@ -36,6 +40,75 @@ function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}

function credentialsUseKeychain(credentialsPath) {
return isDarwin && !fs.existsSync(credentialsPath);
}

function keychainAccount() {
try {
const out = execFileSync('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE], { encoding: 'utf8' });
const match = out.match(/"acct"<blob>="([^"]*)"/);
return match ? match[1] : null;
} catch {
return null;
}
Comment on lines +47 to +54

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [[ "$(uname -s)" != "Darwin" ]]; then
  echo "Run this check on a macOS host with Claude Code credentials."
  exit 0
fi

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

security find-generic-password -s 'Claude Code-credentials' \
  >"$tmpdir/stdout" 2>"$tmpdir/stderr"

if grep -q '"acct"<blob>' "$tmpdir/stdout"; then
  echo 'Account metadata is written to stdout.'
elif grep -q '"acct"<blob>' "$tmpdir/stderr"; then
  echo 'Account metadata is written to stderr.'
else
  echo 'Account metadata was not found in either stream.'
  exit 1
fi

Repository: Leuconoe/ClaudeCodeMultiAccounts

Length of output: 231


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- lib/store/io.cjs ---'
cat -n lib/store/io.cjs | sed -n '1,140p'

printf '%s\n' '--- related security calls and consumers ---'
rg -n -C 3 "keychainAccount|find-generic-password|add-generic-password|execFileSync|backup" lib

Repository: Leuconoe/ClaudeCodeMultiAccounts

Length of output: 11035


🌐 Web query:

macOS security find-generic-password output acct blob stdout stderr

💡 Result:

To retrieve a password from the macOS keychain using the security command-line tool, you primarily use the find-generic-password command [1][2]. Retrieving the Password To output only the password string to standard output (stdout), use the -w flag [1][2]: security find-generic-password -s "YourServiceName" -a "YourAccountName" -w Handling macOS 26 (Tahoe) Hex Encoding As of macOS 26 (Tahoe), the output format of security find-generic-password -w has changed [3]. If the stored password contains binary data or non-ASCII characters, it is now returned as a HEX-encoded string [3]. If you encounter this hex-encoded output in scripts, you must decode it to retrieve the original data [3]. You can use xxd to perform this conversion [3]: # Decode hex output back to binary security find-generic-password -s "YourServiceName" -w | xxd -r -p Additional Options -g: When used instead of or in addition to -w, this flag requests that the password be displayed [1]. Historically, -g outputs metadata and the password in a structured text format, often triggering a GUI prompt to authorize access [4][5]. -a (account): Specifies the account name associated with the generic password [1]. -s (service): Specifies the service name associated with the generic password [1]. Important Considerations - Access Control: The first time the security tool attempts to access a specific keychain item, you will be prompted to grant it permission [2]. You can grant persistent access via the prompt or by adding -T /usr/bin/security when initially creating the item [2]. - Duplicate Entries: The security command returns the first entry it finds that matches the specified -s (service) and -a (account) attributes [6]. If multiple entries exist for the same service but different accounts, or if duplicates have been created, querying by service alone may lead to unpredictable results [6]. - Security Note: Be cautious when storing retrieved passwords in environment variables, as they may be visible to other processes via tools like ps -E [7].

Citations:


🌐 Web query:

Node.js execFileSync return value stdout stderr documentation

💡 Result:

The execFileSync method in Node.js returns the stdout from the executed command as a Buffer or string [1][2][3]. Key behavioral details include: Return Value: When successful, it returns the stdout of the process [1][2]. The type of the return value depends on the encoding option provided; if an encoding (e.g., 'utf8') is specified, it returns a string, otherwise it returns a Buffer [4][1]. Handling Stderr and Errors: 1. Default Behavior: By default, stderr from the child process is piped to the parent process's stderr and is not captured in the return value of execFileSync [4][5]. 2. Capturing Stderr: To capture stderr, you must configure the stdio option (e.g., set stdio: 'pipe') [4][3]. 3. Non-Zero Exit Codes: If the child process exits with a non-zero exit code or times out, execFileSync throws an Error [4][1]. This error object contains the full result of the process execution, including the stdout and stderr properties [4][3]. You can access these by catching the error: const { execFileSync } = require('node:child_process'); try { const stdout = execFileSync('my-script.sh', { stdio: 'pipe', encoding: 'utf8' }); } catch (err) { // If the process fails, access stdout/stderr from the error object const { stdout, stderr } = err; console.error({ stdout, stderr }); }

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- project references and tests ---'
rg -n -C 4 "Claude Code-credentials|keychain|credentialsUseKeychain|writeCredentials|backupKeychainCredentials|macOS" . \
  -g '!node_modules' -g '!dist' -g '!build'

Repository: Leuconoe/ClaudeCodeMultiAccounts

Length of output: 8063


🌐 Web query:

"acct\"<blob>" "security find-generic-password"

💡 Result:

The string acct"= appears in the output of the macOS security command when querying keychain items, specifically when using the -g (dump) flag [1][2]. It is a formatted representation of the account attribute associated with a generic password item in a keychain [3]. When you run security find-generic-password with the -g (or --dump) option, the tool outputs detailed attributes of the found keychain item, often including internal metadata tags formatted like "acct"= [1][4]. Because this output is not designed for direct machine parsing, many scripts use tools like sed, awk, or grep to isolate the account name or password by targeting this specific string pattern [1][5][6]. For example, a common approach to extract the account name from such output involves: 1. Running security find-generic-password -g -s [service_name] to retrieve the full item details [7]. 2. Piping the output to grep to find the line containing acct"= [1]. 3. Using a stream editor like sed or cut to strip away the surrounding metadata and extract only the account value [1][5][6]. Note that this output format is specific to legacy file-based keychains [8]. The security command-line tool does not have access to entries managed by the modern macOS Passwords app (or items stored in the Data Protection/iCloud Keychain backend) [8]. Additionally, extracting passwords programmatically often triggers system authorization prompts, which may require user intervention unless access has been specifically granted to the binary [3][9].

Citations:


🌐 Web query:

site:github.com "security find-generic-password -s" '"acct"<blob>'

💡 Result:

The command security find-generic-password -s 'acct' is used on macOS to retrieve a generic password item from the Keychain that matches a specific service name [1][2][3]. In this command: - find-generic-password is the subcommand used to search for a generic password item [1][4]. - -s 'acct' specifies the service name to match. The -s flag tells the security tool to filter the search by the "service" attribute associated with the keychain item [1][2][3]. While your example includes <blob>, in a functional command, you would replace 'acct' (and the surrounding syntax) with the actual name of the service registered in your Keychain [5][3]. For example, if you stored a password with the service name "MyService", you would run: security find-generic-password -s "MyService" -w The -w flag is commonly used in this context to output only the password itself to standard output, making it useful for scripts and command-line automation [1][5][3]. If you do not use the -w or -g flags, the tool typically outputs descriptive information about the keychain item rather than the password itself [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package scripts and repository tests ---'
cat package.json
find . -maxdepth 4 -type f \( -iname '*test*' -o -iname '*spec*' \) -print

Repository: Leuconoe/ClaudeCodeMultiAccounts

Length of output: 1598


🌐 Web query:

macOS security find-generic-password without -g output attributes stdout "attributes:"

💡 Result:

To retrieve only the password from a generic keychain item using the macOS security command line tool without including extra attributes or metadata, use the -w (or --password) flag [1][2][3]. When this flag is used, the command outputs the raw password string directly to standard output (stdout) [1][2]. Example usage: security find-generic-password -s "YourServiceName" -a "YourAccountName" -w If you do not specify the -w flag, the security tool will output verbose information, including attributes, metadata, and (if the -g flag is also used) the password prefixed with text like "password: " [2][4][5]. Key points regarding this command: - The -w flag is the primary mechanism to suppress attribute output and return only the password value [2][3]. - You may still be prompted for your macOS user password (or Keychain password) to authorize access to the stored secret, depending on your system's access control settings [1][6][7]. - If the command fails to find the item, it will typically print an error to standard error (stderr) and return a non-zero exit code [6]. - Because the password is printed to stdout, ensure you handle it securely (e.g., avoid echoing it or including it in shell history files if possible) [1].

Citations:


🌐 Web query:

site:github.com "security find-generic-password" "attributes:" "acct"

💡 Result:

In the context of the macOS security command line utility, acct is a core attribute tag representing the account name associated with a password item [1]. It corresponds to the SecAccountItemAttr constant in the underlying Apple Security framework [1]. When using the security find-generic-password command, you use the -a flag to specify or match this acct attribute [2]. Usage: To find a password for a specific account and service: security find-generic-password -a <account_name> -s <service_name> -w Key points: 1. Attribute Role: The acct attribute identifies the user account (or username) linked to a keychain item [1]. 2. Command-Line Flag: The -a flag is the standard interface for the acct attribute [2][3]. 3. Output Format: When inspecting keychain items, the acct attribute is often displayed in the output as "acct" =="" [4][5]. 4. Application: It is used in generic, Internet, and AppleShare password items to uniquely identify the account [1]. For example, to retrieve only the password (-w) for a specific service and account, use: security find-generic-password -s "my-service" -a "my-username" -w [3][6]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exported store API and callers ---'
cat -n lib/store/io.cjs | sed -n '129,165p'
rg -n -C 5 "writeLiveState|writeCredentials|readCredentials" --glob '*.cjs' .

Repository: Leuconoe/ClaudeCodeMultiAccounts

Length of output: 6149


Distinguish a missing Keychain item from a failed lookup.

execFileSync returns stdout, and security find-generic-password prints "acct"<blob> in that output. Capture stderr for command diagnostics. Do not convert every lookup error into null. Use the fallback account only when the item is explicitly absent. Propagate access, authorization, and parse errors before writing the new value; otherwise the write can update the wrong item without a backup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/store/io.cjs` around lines 47 - 54, Update keychainAccount to capture
stderr and distinguish an explicitly missing Keychain item from other failures.
Return null only when security reports that the item is absent; otherwise
propagate access, authorization, command, and account-parsing errors so the
caller cannot proceed with an unsafe fallback account.

}

function readKeychainCredentials() {
try {
const raw = execFileSync('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w'], { encoding: 'utf8' });
return JSON.parse(raw.trim());
} catch (err) {
throw new Error(`Failed to read credentials from macOS keychain (service "${KEYCHAIN_SERVICE}"): ${err.message}`);
}
}

function writeKeychainCredentials(value) {
const account = keychainAccount() || os.userInfo().username || 'Claude Code';
execFileSync('security', [
'add-generic-password', '-U',
'-s', KEYCHAIN_SERVICE,
'-a', account,
'-w', JSON.stringify(value),
]);
}

function backupKeychainCredentials(backupDir) {
let value;
try {
value = readKeychainCredentials();
} catch {
return;
}
Comment on lines +76 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Stop the write when Keychain backup fails.

Line 80 suppresses malformed JSON, denied Keychain access, and command failures. writeCredentials then overwrites the item at Line 105. This can destroy the only recoverable credential value without a backup.

Skip backup only when the Keychain item is confirmed absent. Propagate every other read failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/store/io.cjs` around lines 76 - 82, Update backupKeychainCredentials to
distinguish a confirmed missing Keychain item from other read failures: return
without writing only for the established “not found” condition, and propagate
malformed JSON, access-denied, and command errors instead of swallowing them.
Ensure writeCredentials cannot overwrite the existing item when
readKeychainCredentials fails for any reason other than absence.

ensureDir(backupDir);
const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-');
fs.writeFileSync(path.join(backupDir, `credentials-keychain.${timestamp}.bak`), `${JSON.stringify(value, null, 2)}\n`, 'utf8');
const backups = fs.readdirSync(backupDir)
.filter((name) => name.startsWith('credentials-keychain.') && name.endsWith('.bak'))
.sort()
.reverse();
for (const stale of backups.slice(3)) {
fs.rmSync(path.join(backupDir, stale), { force: true });
}
}

function readCredentials(credentialsPath) {
if (credentialsUseKeychain(credentialsPath)) {
return readKeychainCredentials();
}
return readJson(credentialsPath);
}

function writeCredentials(credentialsPath, value, backupDir) {
if (credentialsUseKeychain(credentialsPath)) {
backupKeychainCredentials(backupDir);
writeKeychainCredentials(value);
return;
}
backupFile(credentialsPath, backupDir);
writeJson(credentialsPath, value);
}

function backupFile(filePath, backupDir) {
if (!fs.existsSync(filePath)) return;
ensureDir(backupDir);
Expand All @@ -59,9 +132,8 @@ function deepCopy(value) {

function writeLiveState(config, credentials, options) {
backupFile(options.configPath, options.backupDir);
backupFile(options.credentialsPath, options.backupDir);
writeJson(options.configPath, config);
writeJson(options.credentialsPath, credentials);
writeCredentials(options.credentialsPath, credentials, options.backupDir);
}

function writeStore(store, options) {
Expand All @@ -79,6 +151,8 @@ module.exports = {
readJsonIfExists,
writeJson,
backupFile,
readCredentials,
writeCredentials,
deepCopy,
writeLiveState,
writeStore,
Expand Down