Skip to content

Commit 00e2fa2

Browse files
committed
fix(env): stop claiming ~/.env; read from ~/.config/PAI/.env instead
PAI currently owns the user's ~/.env file via a symlink created during install. This is a home-directory namespace grab: ~/.env is a conventional, user-owned file that many shells and tools look for. If the user installs any other tool that expects to read ~/.env, it either collides with PAI's secrets or is silently overwritten on the next PAI install. This is the wrong shape. The cause is that VoiceServer hardcodes `join(homedir(), '.env')` as the only place it looks for ELEVENLABS_API_KEY. The installer created the ~/.env symlink to make that hardcoded read resolve to the real secrets file at ~/.config/PAI/.env (which is already the XDG-compliant, correct canonical location). This change: - VoiceServer/server.ts: load ~/.config/PAI/.env first (XDG canonical location), then optionally overlay from ~/.env if the user has chosen to put PAI-relevant keys there. Values in ~/.env win on key collisions, preserving the "explicit user override" mental model. The error message when ELEVENLABS_API_KEY is missing now points at the canonical path. - PAI-Install/engine/actions.ts: stop creating the ~/.env symlink. ~/.claude/.env stays symlinked (that path is PAI's own namespace, so the symlink is safe and the hooks' existing reads continue working unchanged). The new comment explains why we no longer touch ~/.env. Backward compatibility: - Existing installs that already have the ~/.env symlink continue to work — both paths point at the same real file, so loading both is a no-op. - Existing installs that already have a real ~/.env with PAI values in it continue to work — loadEnvFile() reads both paths and either one (or both) can contain the keys. - New installs after this change will have a real file at ~/.config/PAI/.env and will NOT touch ~/.env. Users own ~/.env. Related to the VoiceServer cross-platform audio work in danielmiessler#1061 and the Pushcut notification channel in danielmiessler#1062.
1 parent dc05fe9 commit 00e2fa2

2 files changed

Lines changed: 37 additions & 25 deletions

File tree

Releases/v4.0.3/.claude/PAI-Install/engine/actions.ts

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -724,28 +724,29 @@ export async function runConfiguration(
724724
await emit({ event: "message", content: "API keys saved securely." });
725725
}
726726

727-
// Create symlinks so all consumers can find the .env
728-
// Voice server reads ~/.env, hooks read ~/.claude/.env
727+
// Create a symlink at ~/.claude/.env for PAI hooks that still read from
728+
// the claude-scoped path. ~/.claude/ is PAI's own namespace so the symlink
729+
// is safe; we do NOT symlink ~/.env anymore because ~/.env belongs to the
730+
// user — they may have their own shell environment file there unrelated to
731+
// PAI. VoiceServer and other components now read from envPath
732+
// (~/.config/PAI/.env) directly, with an optional overlay from ~/.env if
733+
// the user chooses to put PAI-relevant keys there.
729734
if (existsSync(envPath)) {
730-
const symlinkPaths = [
731-
join(paiDir, ".env"), // ~/.claude/.env
732-
join(homedir(), ".env"), // ~/.env (voice server reads this)
733-
];
734-
for (const symlinkPath of symlinkPaths) {
735-
try {
736-
// Remove stale symlink or file before creating
737-
if (existsSync(symlinkPath)) {
738-
const stat = lstatSync(symlinkPath);
739-
if (stat.isSymbolicLink()) {
740-
unlinkSync(symlinkPath);
741-
} else {
742-
continue; // Don't overwrite a real file
743-
}
735+
const claudeEnvSymlink = join(paiDir, ".env");
736+
try {
737+
if (existsSync(claudeEnvSymlink)) {
738+
const stat = lstatSync(claudeEnvSymlink);
739+
if (stat.isSymbolicLink()) {
740+
unlinkSync(claudeEnvSymlink);
741+
} else {
742+
// Don't overwrite a real file — skip creating the symlink.
744743
}
745-
symlinkSync(envPath, symlinkPath);
746-
} catch {
747-
// Permission error or path conflict
748744
}
745+
if (!existsSync(claudeEnvSymlink)) {
746+
symlinkSync(envPath, claudeEnvSymlink);
747+
}
748+
} catch {
749+
// Permission error or path conflict — non-fatal.
749750
}
750751
}
751752

Releases/v4.0.3/.claude/VoiceServer/server.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ import { homedir } from "os";
2020
import { join } from "path";
2121
import { existsSync, readFileSync } from "fs";
2222

23-
// Load .env from user home directory
24-
const envPath = join(homedir(), '.env');
25-
if (existsSync(envPath)) {
26-
const envContent = await Bun.file(envPath).text();
23+
// Load .env files — XDG-compliant location first, then ~/.env as optional
24+
// user overlay. The user owns ~/.env for their own purposes; PAI-managed
25+
// secrets live at ~/.config/PAI/.env so they never collide with user's file.
26+
// Values in ~/.env win in the case of key collisions (explicit user override).
27+
async function loadEnvFile(path: string): Promise<void> {
28+
if (!existsSync(path)) return;
29+
const envContent = await Bun.file(path).text();
2730
envContent.split('\n').forEach(line => {
2831
const [key, value] = line.split('=');
2932
if (key && value && !key.startsWith('#')) {
@@ -32,12 +35,20 @@ if (existsSync(envPath)) {
3235
});
3336
}
3437

38+
const xdgEnvPath = join(homedir(), '.config', 'PAI', '.env');
39+
const homeEnvPath = join(homedir(), '.env');
40+
41+
// Load PAI's managed secrets first (canonical location)
42+
await loadEnvFile(xdgEnvPath);
43+
// Then user overlay — any key in ~/.env wins over the XDG file
44+
await loadEnvFile(homeEnvPath);
45+
3546
const PORT = parseInt(process.env.PORT || "8888");
3647
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY;
3748

3849
if (!ELEVENLABS_API_KEY) {
39-
console.error('⚠️ ELEVENLABS_API_KEY not found in ~/.env');
40-
console.error('Add: ELEVENLABS_API_KEY=your_key_here');
50+
console.error('⚠️ ELEVENLABS_API_KEY not found');
51+
console.error(`Add it to ${xdgEnvPath} (or ~/.env) as: ELEVENLABS_API_KEY=your_key_here`);
4152
}
4253

4354
// ==========================================================================

0 commit comments

Comments
 (0)