diff --git a/external_plugins/discord/.claude-plugin/plugin.json b/external_plugins/discord/.claude-plugin/plugin.json index 6418b1ec..dcc2bd30 100644 --- a/external_plugins/discord/.claude-plugin/plugin.json +++ b/external_plugins/discord/.claude-plugin/plugin.json @@ -1,11 +1,20 @@ { "name": "discord", "description": "Discord channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /discord:access.", - "version": "0.0.4", + "version": "0.0.5", "keywords": [ "discord", "messaging", "channel", "mcp" - ] + ], + "userConfig": { + "DISCORD_BOT_TOKEN": { + "type": "string", + "title": "Bot Token", + "description": "Bot token from the Discord Developer Portal. Stored in keychain (macOS) or ~/.claude/.credentials.json with 0600 permissions elsewhere. Never written to settings.json.", + "required": true, + "sensitive": true + } + } } diff --git a/external_plugins/discord/.mcp.json b/external_plugins/discord/.mcp.json index 081e9ee5..cfca6093 100644 --- a/external_plugins/discord/.mcp.json +++ b/external_plugins/discord/.mcp.json @@ -2,7 +2,10 @@ "mcpServers": { "discord": { "command": "bun", - "args": ["run", "--cwd", "${CLAUDE_PLUGIN_ROOT}", "--shell=bun", "--silent", "start"] + "args": ["run", "--cwd", "${CLAUDE_PLUGIN_ROOT}", "--shell=bun", "--silent", "start"], + "env": { + "DISCORD_BOT_TOKEN": "${user_config.DISCORD_BOT_TOKEN}" + } } } } diff --git a/external_plugins/discord/server.ts b/external_plugins/discord/server.ts index 1752ebf8..7a81bcc8 100644 --- a/external_plugins/discord/server.ts +++ b/external_plugins/discord/server.ts @@ -39,10 +39,12 @@ const ACCESS_FILE = join(STATE_DIR, 'access.json') const APPROVED_DIR = join(STATE_DIR, 'approved') const ENV_FILE = join(STATE_DIR, '.env') -// Load ~/.claude/channels/discord/.env into process.env. Real env wins. -// Plugin-spawned servers don't get an env block — this is where the token lives. +// Token is injected via ${user_config.DISCORD_BOT_TOKEN} from .mcp.json — +// prompted at enable time, stored in keychain (macOS) or .credentials.json 0600 +// elsewhere. The .env file below is a legacy fallback for users configured +// before H1 #3617646 — real env wins, so the injected value takes precedence. try { - // Token is a credential — lock to owner. No-op on Windows (would need ACLs). + // Defensive chmod for legacy .env files (no-op on Windows). chmodSync(ENV_FILE, 0o600) for (const line of readFileSync(ENV_FILE, 'utf8').split('\n')) { const m = line.match(/^(\w+)=(.*)$/) @@ -56,8 +58,8 @@ const STATIC = process.env.DISCORD_ACCESS_MODE === 'static' if (!TOKEN) { process.stderr.write( `discord channel: DISCORD_BOT_TOKEN required\n` + - ` set in ${ENV_FILE}\n` + - ` format: DISCORD_BOT_TOKEN=MTIz...\n`, + ` re-enter via: /plugin manage → discord → Configure options\n` + + ` (stored in keychain/credentials.json, not settings.json)\n`, ) process.exit(1) } diff --git a/external_plugins/telegram/.claude-plugin/plugin.json b/external_plugins/telegram/.claude-plugin/plugin.json index 2763481f..a505f4db 100644 --- a/external_plugins/telegram/.claude-plugin/plugin.json +++ b/external_plugins/telegram/.claude-plugin/plugin.json @@ -1,11 +1,20 @@ { "name": "telegram", "description": "Telegram channel for Claude Code \u2014 messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.", - "version": "0.0.4", + "version": "0.0.5", "keywords": [ "telegram", "messaging", "channel", "mcp" - ] + ], + "userConfig": { + "TELEGRAM_BOT_TOKEN": { + "type": "string", + "title": "Bot Token", + "description": "Bot token from @BotFather — format is 123456789:AAH... Stored in keychain (macOS) or ~/.claude/.credentials.json with 0600 permissions elsewhere. Never written to settings.json.", + "required": true, + "sensitive": true + } + } } diff --git a/external_plugins/telegram/.mcp.json b/external_plugins/telegram/.mcp.json index cf7195be..6ea6d255 100644 --- a/external_plugins/telegram/.mcp.json +++ b/external_plugins/telegram/.mcp.json @@ -2,7 +2,10 @@ "mcpServers": { "telegram": { "command": "bun", - "args": ["run", "--cwd", "${CLAUDE_PLUGIN_ROOT}", "--shell=bun", "--silent", "start"] + "args": ["run", "--cwd", "${CLAUDE_PLUGIN_ROOT}", "--shell=bun", "--silent", "start"], + "env": { + "TELEGRAM_BOT_TOKEN": "${user_config.TELEGRAM_BOT_TOKEN}" + } } } } diff --git a/external_plugins/telegram/server.ts b/external_plugins/telegram/server.ts index 3211bbae..d13d30af 100644 --- a/external_plugins/telegram/server.ts +++ b/external_plugins/telegram/server.ts @@ -28,10 +28,12 @@ const ACCESS_FILE = join(STATE_DIR, 'access.json') const APPROVED_DIR = join(STATE_DIR, 'approved') const ENV_FILE = join(STATE_DIR, '.env') -// Load ~/.claude/channels/telegram/.env into process.env. Real env wins. -// Plugin-spawned servers don't get an env block — this is where the token lives. +// Token is injected via ${user_config.TELEGRAM_BOT_TOKEN} from .mcp.json — +// prompted at enable time, stored in keychain (macOS) or .credentials.json 0600 +// elsewhere. The .env file below is a legacy fallback for users configured +// before H1 #3617646 — real env wins, so the injected value takes precedence. try { - // Token is a credential — lock to owner. No-op on Windows (would need ACLs). + // Defensive chmod for legacy .env files (no-op on Windows). chmodSync(ENV_FILE, 0o600) for (const line of readFileSync(ENV_FILE, 'utf8').split('\n')) { const m = line.match(/^(\w+)=(.*)$/) @@ -45,8 +47,8 @@ const STATIC = process.env.TELEGRAM_ACCESS_MODE === 'static' if (!TOKEN) { process.stderr.write( `telegram channel: TELEGRAM_BOT_TOKEN required\n` + - ` set in ${ENV_FILE}\n` + - ` format: TELEGRAM_BOT_TOKEN=123456789:AAH...\n`, + ` re-enter via: /plugin manage → telegram → Configure options\n` + + ` (stored in keychain/credentials.json, not settings.json)\n`, ) process.exit(1) }