diff --git a/external_plugins/discord/.claude-plugin/plugin.json b/external_plugins/discord/.claude-plugin/plugin.json index 7447381..f97dd26 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.1", + "description": "Discord channel for Claude Code — messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /discord:access.", + "version": "0.0.2", "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 081e9ee..cfca609 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 078c29a..6929027 100644 --- a/external_plugins/discord/server.ts +++ b/external_plugins/discord/server.ts @@ -34,10 +34,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+)=(.*)$/) @@ -51,8 +53,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`, + ` configure via: /plugin reconfigure discord\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 ac3472e..3593bb4 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.1", + "description": "Telegram channel for Claude Code — messaging bridge with built-in access control. Manage pairing, allowlists, and policy via /telegram:access.", + "version": "0.0.2", "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 cf7195b..6ea6d25 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 8acd52a..a919306 100644 --- a/external_plugins/telegram/server.ts +++ b/external_plugins/telegram/server.ts @@ -27,10 +27,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+)=(.*)$/) @@ -44,8 +46,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`, + ` configure via: /plugin reconfigure telegram\n` + + ` (stored in keychain/credentials.json, not settings.json)\n`, ) process.exit(1) }