From e866cfcbfa2b61c74d4bddfc2ad2144798f501c4 Mon Sep 17 00:00:00 2001 From: jrenaldi Date: Sat, 14 Mar 2026 16:17:13 -0500 Subject: [PATCH 1/6] docs: add design spec for shell-independent API key resolution (#11) Fixes the 'Missing Authentication header' bug in non-interactive shells by loading sidecar's .env and auth.json into process.env at startup. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...026-03-14-shell-independent-keys-design.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-14-shell-independent-keys-design.md diff --git a/docs/superpowers/specs/2026-03-14-shell-independent-keys-design.md b/docs/superpowers/specs/2026-03-14-shell-independent-keys-design.md new file mode 100644 index 0000000..5865257 --- /dev/null +++ b/docs/superpowers/specs/2026-03-14-shell-independent-keys-design.md @@ -0,0 +1,98 @@ +# Shell-Independent API Key Resolution + +**Issue:** [#11](https://github.com/jrenaldi79/sidecar/issues/11) - Sidecar fails with 'Missing Authentication header' in non-interactive shells +**Date:** 2026-03-14 +**Status:** Draft + +## Problem + +Sidecar CLI fails when API keys are exported in `~/.zshrc` but not `~/.zshenv`. Non-interactive shells (Claude Code's Bash tool, CI/CD, cron) don't source `~/.zshrc`, so `process.env` lacks the keys. + +The irony: sidecar already has two shell-independent key stores (`~/.config/sidecar/.env` and `~/.local/share/opencode/auth.json`), but doesn't load them into `process.env` early enough. The validator checks `process.env` first and fails before reaching fallback logic. + +## Design + +### New Module: `src/utils/env-loader.js` + +Single exported function `loadCredentials()` that projects all credential sources into `process.env` with deterministic priority: + +``` +1. process.env (already set) <- highest, never overwritten +2. ~/.config/sidecar/.env <- user-configured via `sidecar setup` +3. ~/.local/share/opencode/auth.json <- OpenCode SDK fallback +``` + +Behavior: +- Per-provider, first source wins. Existing `process.env` values are never overwritten. +- Respects `SIDECAR_ENV_DIR` override for `.env` path (via `getEnvPath()` from `api-key-store.js`). +- Uses existing `parseEnvContent()` from `api-key-store.js` for `.env` parsing (no separate `dotenv` dependency needed). +- Handles `LEGACY_KEY_NAMES` migration (e.g., `GEMINI_API_KEY` to `GOOGLE_GENERATIVE_AI_API_KEY`), consolidating the migration that currently lives in both `bin/sidecar.js` and `api-key-store.js`. +- Auth.json keys are loaded in-memory only (no writes back to `.env`). +- Info-level logging when a key is loaded: `"Loaded GOOGLE_GENERATIVE_AI_API_KEY from sidecar .env"` (key values are never logged). Info-level is intentional: users hitting this bug are unlikely to have `LOG_LEVEL=debug`. +- Gracefully handles missing files (no error if `.env` or `auth.json` don't exist). + +**Relationship to existing `resolveKeyValue()` in `api-key-store.js`:** The existing function gives `.env` file priority over `process.env`. After `loadCredentials()` projects all sources into `process.env`, `resolveKeyValue()` remains correct for its use case (setup UI display), but runtime validation now uses `process.env` as the single source of truth. No conflict because `loadCredentials()` never overwrites existing `process.env` values. + +**Known limitation:** `auth.json` path (`~/.local/share/opencode/auth.json`) follows the Linux XDG layout. This matches OpenCode's own behavior and is not a regression. + +### Update: `bin/sidecar.js` + +Replace the existing early dotenv load and legacy key migration (lines 14-19) with a single `loadCredentials()` call before any validation or command dispatch. This consolidates three scattered concerns (dotenv loading, legacy migration, auth.json import) into one call. + +**MCP entry point:** `sidecar mcp` dispatches through `bin/sidecar.js` via `handleMcp()`, so `loadCredentials()` runs before any MCP tool handler touches API keys. No separate MCP-specific loading needed. + +### Update: `src/utils/validators.js` + +Remove the special auth.json existence check from `validateApiKey()`. After `loadCredentials()` runs, all available keys are in `process.env`, so the validator becomes a pure `process.env` check. No fallback logic needed. + +Improved error message when keys are truly missing: + +``` +Error: GOOGLE_GENERATIVE_AI_API_KEY not found. + +In non-interactive shells (Claude Code, CI), ~/.zshrc is not sourced. +Fix with one of: + - Run `sidecar setup` to store keys in sidecar's config + - Move your export to ~/.zshenv (sourced by all zsh shells) + - Add key to ~/.local/share/opencode/auth.json +``` + +### Update: Documentation + +- `skill/SKILL.md`: Add note under "Option B (Direct)" warning zsh users that `~/.zshrc` exports only work in interactive terminals. Recommend `~/.zshenv` or `sidecar setup`. +- Add troubleshooting entry for this specific scenario. + +## What We're NOT Doing + +- **No sourcing shell profiles.** Executing `~/.zshenv` or `~/.zshrc` from Node.js is fragile, non-portable (bash vs zsh vs fish), and may have side effects or hang in CI. Both Gemini and GPT independently flagged this as "architecturally leaky." +- **No writing back to `.env` from auth.json.** Importing auth.json keys is in-memory only for the current process. Avoids side-effect writes during simple commands like `sidecar list`. +- **No loading arbitrary `.env` from cwd.** Only sidecar's own config directory `.env` is loaded. + +## Testing + +### Unit Tests: `tests/utils/env-loader.test.js` + +- Priority order: `process.env` > `.env` file > `auth.json` +- No-overwrite: existing `process.env` values are preserved +- Missing files: graceful handling when `.env` or `auth.json` don't exist +- `SIDECAR_ENV_DIR` override: respects custom `.env` path +- Per-provider merge: one key from `.env`, another from `auth.json` +- Security: file permissions, no secret logging + +### Update: `tests/utils/validators.test.js` + +- Remove tests for auth.json special-case fallback in `validateApiKey()` +- Add test for improved error message content +- Verify validation passes when keys come from `.env` (loaded via `loadCredentials()`) + +### Integration Test + +- Mocked integration test that calls `loadCredentials()` + `validateApiKey()` with a stubbed `process.env` and temp `.env` file. No real API call needed; just verify the key reaches `process.env` and validation passes. + +## Multi-Model Review + +Design reviewed by Gemini and GPT-4 via sidecar. Both independently recommended Option B with the same priority order. Key feedback incorporated: +- Reject Option C (sourcing shell profiles) as fragile and unsafe +- Centralize loading in a single module +- Fix the "lying validator" that checks auth.json existence without loading keys +- Keep auth.json imports in-memory only From cf5e069ca5249b269d067ea4ab5c815f42ff4c82 Mon Sep 17 00:00:00 2001 From: jrenaldi Date: Sat, 14 Mar 2026 16:25:31 -0500 Subject: [PATCH 2/6] feat: add env-loader for shell-independent credential loading (#11) New module loads API keys from sidecar .env and auth.json into process.env at CLI bootstrap, with priority: env > .env > auth.json. Exports loadEnvEntries from api-key-store for reuse. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 4 +- src/utils/api-key-store.js | 1 + src/utils/env-loader.js | 53 +++++++++++ tests/env-loader.test.js | 182 +++++++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/utils/env-loader.js create mode 100644 tests/env-loader.test.js diff --git a/CLAUDE.md b/CLAUDE.md index c6c00fb..46c411b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,6 +104,7 @@ src/ │ ├── api-key-validation.js # Validation endpoints per provider │ ├── auth-json.js # Known provider IDs that map to sidecar's PROVIDER_ENV_MAP │ ├── config.js # Default model alias map — short names to full OpenRouter model identifiers +│ ├── env-loader.js # Credential Loader │ ├── idle-watchdog.js # @type {Object.} Default timeouts per mode in milliseconds │ ├── input-validators.js # MCP input validation with structured error responses. │ ├── logger.js # Structured Logger Module @@ -225,10 +226,11 @@ evals/ | `sidecar/start.js` | Generate a unique 8-character hex task ID | `generateTaskId()`, `createSessionMetadata()`, `buildMcpConfig()`, `checkElectronAvailable()`, `runInteractive()` | | `utils/agent-mapping.js` | * All OpenCode native agent names (lowercase) | `PRIMARY_AGENTS()`, `OPENCODE_AGENTS()`, `HEADLESS_SAFE_AGENTS()`, `mapAgentToOpenCode()`, `isValidAgent()` | | `utils/alias-resolver.js` | Alias Resolver Utilities | `applyDirectApiFallback()`, `autoRepairAlias()` | -| `utils/api-key-store.js` | Maps provider IDs to environment variable names | `getEnvPath()`, `readApiKeys()`, `readApiKeyHints()`, `readApiKeyValues()`, `saveApiKey()` | +| `utils/api-key-store.js` | Maps provider IDs to environment variable names | `getEnvPath()`, `loadEnvEntries()`, `readApiKeys()`, `readApiKeyHints()`, `readApiKeyValues()` | | `utils/api-key-validation.js` | Validation endpoints per provider | `validateApiKey()`, `validateOpenRouterKey()`, `VALIDATION_ENDPOINTS()` | | `utils/auth-json.js` | Known provider IDs that map to sidecar's PROVIDER_ENV_MAP | `readAuthJsonKeys()`, `importFromAuthJson()`, `checkAuthJson()`, `removeFromAuthJson()`, `AUTH_JSON_PATH()` | | `utils/config.js` | Default model alias map — short names to full OpenRouter model identifiers | `getConfigDir()`, `getConfigPath()`, `loadConfig()`, `saveConfig()`, `getDefaultAliases()` | +| `utils/env-loader.js` | Credential Loader | `loadCredentials()` | | `utils/idle-watchdog.js` | @type {Object.} Default timeouts per mode in milliseconds | `IdleWatchdog()`, `resolveTimeout()` | | `utils/input-validators.js` | MCP input validation with structured error responses. | `validateStartInputs()`, `findSimilar()` | | `utils/logger.js` | Structured Logger Module | `logger()`, `LOG_LEVELS()` | diff --git a/src/utils/api-key-store.js b/src/utils/api-key-store.js index 0437a83..de5bd82 100644 --- a/src/utils/api-key-store.js +++ b/src/utils/api-key-store.js @@ -233,6 +233,7 @@ function removeApiKey(provider) { module.exports = { getEnvPath, + loadEnvEntries, readApiKeys, readApiKeyHints, readApiKeyValues, diff --git a/src/utils/env-loader.js b/src/utils/env-loader.js new file mode 100644 index 0000000..e0261e6 --- /dev/null +++ b/src/utils/env-loader.js @@ -0,0 +1,53 @@ +/** + * Credential Loader + * + * Loads API keys from multiple sources into process.env at CLI bootstrap. + * Priority: process.env (already set) > sidecar .env > auth.json + * Never overwrites existing process.env values. + */ +const { logger } = require('./logger'); +const { loadEnvEntries, PROVIDER_ENV_MAP, LEGACY_KEY_NAMES } = require('./api-key-store'); +const { readAuthJsonKeys } = require('./auth-json'); + +/** + * Load credentials from all sources into process.env. + * Call once at CLI startup, before any validation. + * + * Sources (in priority order): + * 1. process.env - already set, never overwritten + * 2. ~/.config/sidecar/.env - user-configured via `sidecar setup` + * 3. ~/.local/share/opencode/auth.json - OpenCode SDK fallback + */ +function loadCredentials() { + // Step 1: Load from sidecar .env file + const fileEntries = loadEnvEntries(); + for (const [, envVar] of Object.entries(PROVIDER_ENV_MAP)) { + if (!process.env[envVar]) { + const fromFile = fileEntries.get(envVar); + if (fromFile && fromFile.length > 0) { + process.env[envVar] = fromFile; + logger.info(`Loaded ${envVar} from sidecar .env`); + } + } + } + + // Step 1b: Handle legacy key names in process.env + for (const [oldName, newName] of Object.entries(LEGACY_KEY_NAMES)) { + if (process.env[oldName] && !process.env[newName]) { + process.env[newName] = process.env[oldName]; + logger.info(`Migrated ${oldName} to ${newName}`); + } + } + + // Step 2: Import from auth.json (lowest priority) + const authKeys = readAuthJsonKeys(); + for (const [provider, key] of Object.entries(authKeys)) { + const envVar = PROVIDER_ENV_MAP[provider]; + if (envVar && !process.env[envVar]) { + process.env[envVar] = key; + logger.info(`Loaded ${envVar} from auth.json`); + } + } +} + +module.exports = { loadCredentials }; diff --git a/tests/env-loader.test.js b/tests/env-loader.test.js new file mode 100644 index 0000000..e87917e --- /dev/null +++ b/tests/env-loader.test.js @@ -0,0 +1,182 @@ +/** + * Tests for src/utils/env-loader.js + * + * Verifies credential loading from multiple sources into process.env + * with deterministic priority: process.env > sidecar .env > auth.json + */ +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// Must require AFTER setting env vars in beforeEach +let loadCredentials; + +describe('env-loader', () => { + let tmpDir; + let originalEnv; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sidecar-envloader-')); + originalEnv = { ...process.env }; + process.env.SIDECAR_ENV_DIR = tmpDir; + // Clear all provider keys + delete process.env.OPENROUTER_API_KEY; + delete process.env.GOOGLE_GENERATIVE_AI_API_KEY; + delete process.env.OPENAI_API_KEY; + delete process.env.ANTHROPIC_API_KEY; + delete process.env.DEEPSEEK_API_KEY; + delete process.env.GEMINI_API_KEY; + // Fresh require each test to avoid module caching + jest.resetModules(); + loadCredentials = require('../src/utils/env-loader').loadCredentials; + }); + + afterEach(() => { + process.env = originalEnv; + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('should load keys from sidecar .env into process.env', () => { + fs.writeFileSync( + path.join(tmpDir, '.env'), + 'OPENROUTER_API_KEY=sk-or-from-file\n', + { mode: 0o600 } + ); + + loadCredentials(); + + expect(process.env.OPENROUTER_API_KEY).toBe('sk-or-from-file'); + }); + + it('should NOT overwrite existing process.env values', () => { + process.env.OPENROUTER_API_KEY = 'sk-or-from-env'; + fs.writeFileSync( + path.join(tmpDir, '.env'), + 'OPENROUTER_API_KEY=sk-or-from-file\n', + { mode: 0o600 } + ); + + loadCredentials(); + + expect(process.env.OPENROUTER_API_KEY).toBe('sk-or-from-env'); + }); + + it('should load keys from auth.json when not in .env', () => { + jest.resetModules(); + jest.doMock('../src/utils/auth-json', () => ({ + readAuthJsonKeys: () => ({ openrouter: 'sk-or-from-auth' }), + AUTH_JSON_PATH: '/tmp/fake/auth.json', + KNOWN_PROVIDERS: ['openrouter', 'google', 'openai', 'anthropic', 'deepseek'] + })); + loadCredentials = require('../src/utils/env-loader').loadCredentials; + + loadCredentials(); + + expect(process.env.OPENROUTER_API_KEY).toBe('sk-or-from-auth'); + }); + + it('should prefer .env over auth.json', () => { + fs.writeFileSync( + path.join(tmpDir, '.env'), + 'OPENROUTER_API_KEY=sk-or-from-file\n', + { mode: 0o600 } + ); + + jest.resetModules(); + jest.doMock('../src/utils/auth-json', () => ({ + readAuthJsonKeys: () => ({ openrouter: 'sk-or-from-auth' }), + AUTH_JSON_PATH: '/tmp/fake/auth.json', + KNOWN_PROVIDERS: ['openrouter', 'google', 'openai', 'anthropic', 'deepseek'] + })); + loadCredentials = require('../src/utils/env-loader').loadCredentials; + + loadCredentials(); + + expect(process.env.OPENROUTER_API_KEY).toBe('sk-or-from-file'); + }); + + it('should merge keys from different sources per-provider', () => { + fs.writeFileSync( + path.join(tmpDir, '.env'), + 'OPENROUTER_API_KEY=sk-or-from-file\n', + { mode: 0o600 } + ); + + jest.resetModules(); + jest.doMock('../src/utils/auth-json', () => ({ + readAuthJsonKeys: () => ({ google: 'goog-from-auth' }), + AUTH_JSON_PATH: '/tmp/fake/auth.json', + KNOWN_PROVIDERS: ['openrouter', 'google', 'openai', 'anthropic', 'deepseek'] + })); + loadCredentials = require('../src/utils/env-loader').loadCredentials; + + loadCredentials(); + + expect(process.env.OPENROUTER_API_KEY).toBe('sk-or-from-file'); + expect(process.env.GOOGLE_GENERATIVE_AI_API_KEY).toBe('goog-from-auth'); + }); + + it('should handle missing .env file gracefully', () => { + expect(() => loadCredentials()).not.toThrow(); + }); + + it('should handle missing auth.json gracefully', () => { + jest.resetModules(); + jest.doMock('../src/utils/auth-json', () => ({ + readAuthJsonKeys: () => ({}), + AUTH_JSON_PATH: '/nonexistent/auth.json', + KNOWN_PROVIDERS: ['openrouter', 'google', 'openai', 'anthropic', 'deepseek'] + })); + loadCredentials = require('../src/utils/env-loader').loadCredentials; + + expect(() => loadCredentials()).not.toThrow(); + }); + + it('should migrate legacy GEMINI_API_KEY from process.env', () => { + process.env.GEMINI_API_KEY = 'legacy-key'; + + loadCredentials(); + + expect(process.env.GOOGLE_GENERATIVE_AI_API_KEY).toBe('legacy-key'); + }); + + it('should be idempotent (safe to call multiple times)', () => { + fs.writeFileSync( + path.join(tmpDir, '.env'), + 'OPENROUTER_API_KEY=sk-or-from-file\n', + { mode: 0o600 } + ); + + loadCredentials(); + loadCredentials(); + + expect(process.env.OPENROUTER_API_KEY).toBe('sk-or-from-file'); + }); + + describe('integration with validateApiKey', () => { + it('should pass validation when key comes from .env file', () => { + fs.writeFileSync( + path.join(tmpDir, '.env'), + 'OPENROUTER_API_KEY=sk-or-from-file\n', + { mode: 0o600 } + ); + + loadCredentials(); + + const { validateApiKey } = require('../src/utils/validators'); + const result = validateApiKey('openrouter/google/gemini-2.5-flash'); + expect(result.valid).toBe(true); + }); + + it('should fail with actionable error when key is truly missing', () => { + loadCredentials(); + + const { validateApiKey } = require('../src/utils/validators'); + const result = validateApiKey('openrouter/google/gemini-2.5-flash'); + expect(result.valid).toBe(false); + expect(result.error).toContain('OPENROUTER_API_KEY not found'); + expect(result.error).toContain('sidecar setup'); + expect(result.error).toContain('~/.zshenv'); + }); + }); +}); From 13bb1f8f5840081aceed9408507170d4023f171f Mon Sep 17 00:00:00 2001 From: jrenaldi Date: Sat, 14 Mar 2026 16:26:57 -0500 Subject: [PATCH 3/6] fix: shell-independent API key resolution (#11) Replace dotenv + legacy migration in CLI with loadCredentials(). Simplify validateApiKey() to pure process.env check with actionable error message mentioning sidecar setup, ~/.zshenv, and auth.json. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/sidecar.js | 13 +++---------- src/utils/validators.js | 23 +++++++++-------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/bin/sidecar.js b/bin/sidecar.js index 5b288f5..ab931a4 100755 --- a/bin/sidecar.js +++ b/bin/sidecar.js @@ -7,16 +7,9 @@ * Routes commands to appropriate handlers. */ -const path = require('path'); - -// Load API keys from ~/.config/sidecar/.env (single source of truth) -const homeDir = process.env.HOME || process.env.USERPROFILE; -require('dotenv').config({ path: path.join(homeDir, '.config', 'sidecar', '.env'), quiet: true }); - -// Migrate legacy env var: GEMINI_API_KEY -> GOOGLE_GENERATIVE_AI_API_KEY -if (process.env.GEMINI_API_KEY && !process.env.GOOGLE_GENERATIVE_AI_API_KEY) { - process.env.GOOGLE_GENERATIVE_AI_API_KEY = process.env.GEMINI_API_KEY; -} +// Load API keys from all sources: process.env > sidecar .env > auth.json +const { loadCredentials } = require('../src/utils/env-loader'); +loadCredentials(); const { parseArgs, validateStartArgs, getUsage } = require('../src/cli'); const { validateTaskId } = require('../src/utils/validators'); diff --git a/src/utils/validators.js b/src/utils/validators.js index a291545..12773a4 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -226,12 +226,10 @@ const { validateMcpSpec, validateMcpConfigFile } = require('./mcp-validators'); const { MODEL_THINKING_SUPPORT, getSupportedThinkingLevels, validateThinkingLevel } = require('./thinking-validators'); /** - * Validate API key is present for the given model's provider + * Validate API key is present for the given model's provider. * - * NOTE: OpenCode manages its own credentials via `opencode auth`. - * The env var check is a convenience hint, not a hard requirement. - * If OpenCode has credentials configured, the model will work - * even without the env var set. + * Assumes loadCredentials() has already run, projecting all credential + * sources (sidecar .env, auth.json) into process.env. * * @param {string} model - The model string (e.g., 'openrouter/google/gemini-2.5-flash') * @returns {{valid: boolean, error?: string}} @@ -249,17 +247,14 @@ function validateApiKey(model) { } if (!process.env[providerInfo.key]) { - // Check if OpenCode has credentials configured (auth.json) - const os = require('os'); - const authPath = path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json'); - if (fs.existsSync(authPath)) { - // OpenCode manages its own auth — skip env var check - return { valid: true }; - } - return { valid: false, - error: `Error: ${providerInfo.key} environment variable is required for ${providerInfo.name} models. Set it with: export ${providerInfo.key}=your-api-key` + error: `Error: ${providerInfo.key} not found.\n\n` + + 'In non-interactive shells (Claude Code, CI), ~/.zshrc is not sourced.\n' + + 'Fix with one of:\n' + + ' - Run `sidecar setup` to store keys in sidecar\'s config\n' + + ' - Move your export to ~/.zshenv (sourced by all zsh shells)\n' + + ' - Add key to ~/.local/share/opencode/auth.json' }; } From 61ff89596c2bd29c46cee0335e6a3245201829e5 Mon Sep 17 00:00:00 2001 From: jrenaldi Date: Sat, 14 Mar 2026 16:27:57 -0500 Subject: [PATCH 4/6] docs: warn zsh users about ~/.zshrc vs ~/.zshenv (#11) Add zsh-specific note to Option B setup and troubleshooting entry for "Missing Authentication header" in non-interactive shells. Co-Authored-By: Claude Opus 4.6 (1M context) --- skill/SKILL.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/skill/SKILL.md b/skill/SKILL.md index b440434..1990955 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -108,6 +108,10 @@ export ANTHROPIC_API_KEY=your-anthropic-api-key Add these to your shell profile (`~/.bashrc`, `~/.zshrc`) for persistence. +**zsh users:** `~/.zshrc` is only sourced by interactive shells. If you use sidecar from Claude Code, CI, or scripts, either: +- Run `sidecar setup` to store keys in sidecar's config (recommended) +- Move your exports to `~/.zshenv` (sourced by all zsh shell types) + **Model names with direct API keys:** When using direct API keys, use the provider/model format WITHOUT the `openrouter/` prefix: ```bash @@ -867,6 +871,13 @@ The mutex approach looks correct. Add tests." ## Troubleshooting +### "Missing Authentication header" in Claude Code or CI + +API keys in `~/.zshrc` are not available in non-interactive shells. Fix: +1. Run `sidecar setup` (stores keys in `~/.config/sidecar/.env`) +2. Or move exports to `~/.zshenv` +3. Or add credentials to `~/.local/share/opencode/auth.json` + ### "No Claude Code conversation history found" Your project path encoding may not match. Check: From 3dc379f72856229c0df722fa563466791a70fd22 Mon Sep 17 00:00:00 2001 From: jrenaldi Date: Sat, 14 Mar 2026 16:35:05 -0500 Subject: [PATCH 5/6] fix: exclude .worktrees/ from Jest test discovery Prevents Jest from running duplicate tests from git worktree directories. Applied to jest.config.js and all package.json test scripts that override testPathIgnorePatterns. Co-Authored-By: Claude Opus 4.6 (1M context) --- jest.config.js | 3 ++- package.json | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/jest.config.js b/jest.config.js index 151799c..5775ed5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,7 +3,8 @@ module.exports = { testMatch: ['**/tests/**/*.test.js'], testPathIgnorePatterns: [ '/node_modules/', - '\\.integration\\.test\\.js$' + '\\.integration\\.test\\.js$', + '\\.worktrees/' ], collectCoverageFrom: [ 'src/**/*.js', diff --git a/package.json b/package.json index 0fd3ee2..1ed26d7 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "scripts": { "start": "node --experimental-top-level-await --experimental-vm-modules bin/sidecar.js", "test": "jest", - "test:integration": "jest --testPathIgnorePatterns='[]' --testMatch='**/tests/**/*.integration.test.js'", - "test:all": "jest --testPathIgnorePatterns='/node_modules/' && git rev-parse HEAD > .test-passed 2>/dev/null || true", + "test:integration": "jest --testPathIgnorePatterns='\\.worktrees/' --testMatch='**/tests/**/*.integration.test.js'", + "test:all": "jest --testPathIgnorePatterns='/node_modules/' --testPathIgnorePatterns='\\.worktrees/' && git rev-parse HEAD > .test-passed 2>/dev/null || true", "test:e2e:mcp": "jest tests/mcp-repomix-e2e.integration.test.js --testTimeout=180000 --forceExit", "posttest": "git rev-parse HEAD > .test-passed 2>/dev/null || true", "lint": "eslint src/", From 901f40b35dc695c9ea7887573fa781c0a7ec4e23 Mon Sep 17 00:00:00 2001 From: jrenaldi Date: Sat, 14 Mar 2026 16:53:54 -0500 Subject: [PATCH 6/6] docs: fix spec test path and add precedence note (#11) Address CodeRabbit review feedback: correct test file path in spec, add credential resolution order to troubleshooting entry. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../specs/2026-03-14-shell-independent-keys-design.md | 2 +- skill/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/specs/2026-03-14-shell-independent-keys-design.md b/docs/superpowers/specs/2026-03-14-shell-independent-keys-design.md index 5865257..eca7e6f 100644 --- a/docs/superpowers/specs/2026-03-14-shell-independent-keys-design.md +++ b/docs/superpowers/specs/2026-03-14-shell-independent-keys-design.md @@ -70,7 +70,7 @@ Fix with one of: ## Testing -### Unit Tests: `tests/utils/env-loader.test.js` +### Unit Tests: `tests/env-loader.test.js` - Priority order: `process.env` > `.env` file > `auth.json` - No-overwrite: existing `process.env` values are preserved diff --git a/skill/SKILL.md b/skill/SKILL.md index 1990955..eaf0d47 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -873,7 +873,7 @@ The mutex approach looks correct. Add tests." ### "Missing Authentication header" in Claude Code or CI -API keys in `~/.zshrc` are not available in non-interactive shells. Fix: +API keys in `~/.zshrc` are not available in non-interactive shells. Resolution order: `process.env` > `~/.config/sidecar/.env` > `~/.local/share/opencode/auth.json` (first wins). Fix: 1. Run `sidecar setup` (stores keys in `~/.config/sidecar/.env`) 2. Or move exports to `~/.zshenv` 3. Or add credentials to `~/.local/share/opencode/auth.json`