create-secret
Securely store encrypted API keys, tokens, and passwords with AES-256 encryption. Organize secrets by environment tags and set expiration times for controlled access management.
Instructions
Create a new secret. Secrets with the same name can coexist if they have different env tags.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | Name for the secret (e.g. STRIPE_SECRET_KEY) | |
| value | Yes | The secret value to encrypt and store | |
| description | No | Human-readable description of what this secret is for | |
| tags | No | Tags as key:value pairs, e.g. { "env": "production", "project": "acme" } | |
| domain | No | Associated domain for browser extension, e.g. "api.stripe.com" | |
| ttlHours | No | Time-to-live in hours. Omit for permanent (no expiry) |
Implementation Reference
- src/index.ts:466-1489 (handler)The implementation of the "create-secret" tool, which uses the SecureCodeClient to create a new secret.
// Tool: create-secret server.tool( 'create-secret', 'Create a new secret. Secrets with the same name can coexist if they have different env tags.', { name: z.string().describe('Name for the secret (e.g. STRIPE_SECRET_KEY)'), value: z.string().describe('The secret value to encrypt and store'), description: z.string().optional().describe('Human-readable description of what this secret is for'), tags: z.record(z.string(), z.string()).optional().describe('Tags as key:value pairs, e.g. { "env": "production", "project": "acme" }'), domain: z.string().optional().describe('Associated domain for browser extension, e.g. "api.stripe.com"'), ttlHours: z.number().positive().optional().describe('Time-to-live in hours. Omit for permanent (no expiry)'), }, async ({ name, value, description, tags, domain, ttlHours }) => { try { const secret = await getClient().createSecret({ name, value, description, tags, domain, ttlHours }); const info = secret.expiresAt ? `(expires: ${new Date(secret.expiresAt).toLocaleDateString()})` : '(permanent)'; return wrapResponse([{ type: 'text', text: `Secret "${secret.name}" created successfully ${info}` }]); } catch (error) { return errorResult(error); } } ); // Tool: renew-secret server.tool( 'renew-secret', 'Renew an expired secret or change its TTL. Use this to reactivate expired secrets or extend expiration.', { name: z.string().describe('The name of the secret to renew'), ttlHours: z.number().positive().optional().describe('New TTL in hours. Omit to make permanent (no expiry)'), tags: z.record(z.string(), z.string()).optional().describe('Filter tags to disambiguate same-name secrets'), }, async ({ name, ttlHours, tags }) => { try { const secret = await getClient().renewSecret(name, ttlHours, tags); return wrapResponse([{ type: 'text', text: `Secret "${secret.name}" renewed successfully${ttlHours ? ` (expires in ${ttlHours}h)` : ' (permanent)'}` }]); } catch (error) { return errorResult(error); } } ); // Tool: delete-secret server.tool( 'delete-secret', 'Delete a secret (soft delete, can be recovered)', { name: z.string().describe('The name of the secret to delete'), tags: z.record(z.string(), z.string()).optional().describe('Filter tags to disambiguate same-name secrets'), }, async ({ name, tags }) => { try { await getClient().deleteSecret(name, tags); return wrapResponse([{ type: 'text', text: `Secret "${name}" deleted successfully` }]); } catch (error) { return errorResult(error); } } ); // Tool: update-secret server.tool( 'update-secret', 'Update an existing secret\'s value, description, tags, or domain. Only provided fields are changed.', { name: z.string().describe('The name of the secret to update'), value: z.string().optional().describe('New secret value (omit to keep current value)'), description: z.string().optional().describe('New description'), tags: z.record(z.string(), z.string()).optional().describe('New tags (replaces ALL existing tags)'), domain: z.string().optional().describe('New domain'), filterTags: z.record(z.string(), z.string()).optional().describe('Filter tags to disambiguate same-name secrets (different from the "tags" field which sets new tags)'), }, async ({ name, value, description, tags, domain, filterTags }) => { try { const secret = await getClient().updateSecret(name, { value, description, tags, domain }, filterTags); return wrapResponse([{ type: 'text', text: `Secret "${secret.name}" updated successfully` }]); } catch (error) { return errorResult(error); } } ); // Tool: import-env server.tool( 'import-env', 'Import secrets from .env files into SecureCode. For security, .env import must be done through the web interface — the AI agent never sees secret values during import. This tool provides the link to the import page.', {}, async () => { return wrapResponse([{ type: 'text', text: [ 'For security, .env import must be done through the web interface.', 'The AI agent never sees secret values during import.', '', `Open the SecureCode dashboard to import: ${baseUrl}/en/secrets`, '', 'Click the "Import" button, then drop your .env file or paste its content.', 'Choose a project name and environment to tag the imported secrets.', ].join('\n'), }]); } ); // Tool: export-env server.tool( 'export-env', 'Export all secrets as .env or CSV format. Returns the full content with decrypted values.', { format: z.enum(['env', 'csv']).optional().describe('Export format: "env" (default) or "csv"'), tags: z.record(z.string(), z.string()).optional().describe('Filter by tags, e.g. { "env": "production" }'), }, async ({ format, tags }) => { try { const content = await getClient().exportEnv({ format: format || 'env', tags }); if (!content || !content.trim()) { return wrapResponse([{ type: 'text', text: 'No secrets to export.' }]); } const lineCount = content.split('\n').filter(l => l.trim() && !l.startsWith('#')).length; return wrapResponse([{ type: 'text', text: `Exported ${lineCount} secrets (${format || 'env'} format):\n\n${content}` }]); } catch (error) { return errorResult(error); } } ); // Tool: get-status server.tool( 'get-status', 'Get your SecureCodeHQ account status: plan, usage limits, secrets count, and MCP server version. Useful to check remaining capacity and verify the server is up to date.', {}, async () => { try { const client = getClient(); const usage = await client.getUsage(); const secretsLimit = usage.maxSecrets === -1 ? 'unlimited' : `${usage.maxSecrets}`; const accessLimit = usage.maxAccessesPerMonth === -1 ? 'unlimited' : `${usage.maxAccessesPerMonth}`; const lines = [ `MCP Server: v${MCP_VERSION}`, `Plan: ${usage.plan}`, `Secrets: ${usage.secretsCount} / ${secretsLimit}`, `Accesses this month: ${usage.accessesThisMonth} / ${accessLimit}`, ]; if (usage.alerts && usage.alerts.length > 0) { lines.push('', 'Alerts:'); for (const alert of usage.alerts) { lines.push(` ⚠ ${alert.message}`); } } return wrapResponse([{ type: 'text', text: lines.join('\n') }]); } catch (error) { return errorResult(error); } } ); // Tool: wake-session server.tool( 'wake-session', 'Wake (unlock) the session to start accessing secrets. Optionally restrict access to specific tag scope and set auto-sleep timer.', { scope: z.array(z.record(z.string(), z.string())).optional() .describe('Tag filters to restrict which secrets are accessible. E.g. [{project:"acme"}]. Omit for all secrets.'), autoSleepMinutes: z.number().int().min(1).max(1440).optional() .describe('Minutes of inactivity before auto-sleep. Default: 120 (2 hours).'), }, async ({ scope, autoSleepMinutes }) => { try { const client = getClient(); const session = await client.wakeSession({ scope, autoSleepMinutes }); const scopeText = session.scope ? session.scope.map(f => Object.entries(f).map(([k, v]) => `${k}:${v}`).join(' AND ')).join(' OR ') : 'All secrets'; return wrapResponse([{ type: 'text' as const, text: `Session unlocked\n\nStatus: Active\nScope: ${scopeText}\nAuto-sleep: ${session.autoSleepMinutes} minutes\nTime remaining: ${session.autoSleepMinutes} min`, }]); } catch (e) { return errorResult(e); } }, ); // Tool: sleep-session server.tool( 'sleep-session', 'Lock the session immediately. All secret access will be blocked until you wake it again. Use this when you finish working.', {}, async () => { try { const client = getClient(); await client.sleepSession(); cleanupSessionFiles(); return wrapResponse([{ type: 'text' as const, text: 'Session locked. All secret access is now blocked. Injected secrets cleaned from disk.\n\nUse wake-session to unlock when you start working again.', }]); } catch (e) { return errorResult(e); } }, ); // Tool: session-status server.tool( 'session-status', 'Check the current session status: active or sleeping, scope restrictions, and time remaining before auto-sleep.', {}, async () => { try { const client = getClient(); const s = await client.getSessionStatus(); const scopeText = s.scope ? s.scope.map(f => Object.entries(f).map(([k, v]) => `${k}:${v}`).join(' AND ')).join(' OR ') : 'All secrets'; const timeText = s.timeRemainingMinutes !== null ? `${s.timeRemainingMinutes} min` : 'N/A'; const lines = [ `Session: ${s.status === 'active' ? 'Active' : 'Sleeping (locked)'}`, `Scope: ${scopeText}`, `Auto-sleep: ${s.autoSleepMinutes} min`, ]; if (s.status === 'active') { lines.push(`Time remaining: ${timeText}`); } if (s.wakeAt) lines.push(`Last wake: ${new Date(s.wakeAt).toLocaleString()}`); if (s.sleepAt) lines.push(`Last sleep: ${new Date(s.sleepAt).toLocaleString()}`); return wrapResponse([{ type: 'text' as const, text: lines.join('\n') }]); } catch (e) { return errorResult(e); } }, ); // Tool: byebye server.tool( 'byebye', 'End your work session. Locks the session, cleans up all injected secrets from disk, and says goodbye. Use this when you finish working for the day.', {}, async () => { try { const client = getClient(); await client.sleepSession(); } catch { // Session might already be sleeping } cleanupSessionFiles(); return wrapResponse([{ type: 'text' as const, text: 'Session locked & secrets cleaned from disk. See you next time!', }]); }, ); // Tool: get-active-rules (read-only) server.tool( 'get-active-rules', 'List active MCP access rules. Read-only — rules can only be created or modified from the dashboard. Use this to understand why access to a secret was blocked.', {}, async () => { try { const client = getClient(); const rules = await client.getActiveRules(); if (rules.length === 0) { return wrapResponse([{ type: 'text', text: 'No active MCP rules configured.' }]); } const lines = rules.map(r => { const condStr = r.conditions .map(c => `${c.tagKey}:${c.tagValue}`) .join(r.conditionOperator === 'AND' ? ' AND ' : ' OR '); return `• ${r.name} — when [${condStr}] → ${r.action}`; }); return wrapResponse([{ type: 'text', text: `${rules.length} active rules:\n${lines.join('\n')}\n\nManage rules at securecodehq.com/dashboard/mcp-rules`, }]); } catch (error) { return errorResult(error); } }, ); // Tool: onboard (guided onboarding from Claude Code) // State: stores the current onboarding token for multi-call progress let onboardingToken: string | null = null; // Helper: call onboarding API directly (works without API key) async function onboardingFetch<T>(path: string, options?: RequestInit): Promise<T> { const apiBase = `${baseUrl}/api`; const headers: Record<string, string> = { 'Content-Type': 'application/json' }; if (hasApiKey) headers['Authorization'] = `Bearer ${apiKey}`; const res = await fetch(`${apiBase}${path}`, { ...options, headers: { ...headers, ...options?.headers } }); return res.json() as Promise<T>; } // Helper: write .securecoderc file with API key and optional project/env config function writeSecurecodeRc(apiKeyValue: string, project: string | null, env: string | null): void { const rcPath = join(process.cwd(), '.securecoderc'); const lines = ['# SecureCode config (auto-generated, DO NOT commit)']; lines.push(`SECURECODE_API_KEY=${apiKeyValue}`); if (project) lines.push(`SECURECODE_PROJECT=${project}`); if (env) lines.push(`SECURECODE_ENV=${env}`); writeFileSync(rcPath, lines.join('\n') + '\n', { mode: 0o600 }); } // Helper: ensure .securecoderc is listed in .gitignore function updateGitignore(): void { const gitignorePath = join(process.cwd(), '.gitignore'); const entry = '.securecoderc'; try { if (existsSync(gitignorePath)) { const content = readFileSync(gitignorePath, 'utf-8'); // Check if already listed (exact line match) const lines = content.split(/\r?\n/); if (lines.some(line => line.trim() === entry)) return; // Append with a newline separator if file doesn't end with one const separator = content.endsWith('\n') ? '' : '\n'; writeFileSync(gitignorePath, content + separator + entry + '\n'); } else { writeFileSync(gitignorePath, entry + '\n'); } } catch { // Best-effort — don't break onboarding if .gitignore fails } } // Module-level vars to store project/env from onboarding import result let onboardingProject: string | null = null; let onboardingEnv: string | null = null; // ── Onboarding token persistence ────────────────────────────────────── // Persist onboarding token in .securecoderc so it survives Claude Code restarts. // Cleaned up when onboarding completes or token expires. function readOnboardingTokenFromRc(): string | null { try { const rcPath = join(process.cwd(), '.securecoderc'); if (!existsSync(rcPath)) return null; const content = readFileSync(rcPath, 'utf-8'); for (const line of content.split('\n')) { const trimmed = line.trim(); if (trimmed.startsWith('SECURECODE_ONBOARDING_TOKEN=')) { return trimmed.substring('SECURECODE_ONBOARDING_TOKEN='.length).trim() || null; } } } catch { /* ignore */ } return null; } function writeOnboardingTokenToRc(token: string): void { try { const rcPath = join(process.cwd(), '.securecoderc'); if (existsSync(rcPath)) { let content = readFileSync(rcPath, 'utf-8'); // Remove existing token line if present const lines = content.split('\n').filter(l => !l.trim().startsWith('SECURECODE_ONBOARDING_TOKEN=')); lines.push(`SECURECODE_ONBOARDING_TOKEN=${token}`); writeFileSync(rcPath, lines.join('\n') + (lines[lines.length - 1] === '' ? '' : '\n'), { mode: 0o600 }); } else { writeFileSync(rcPath, `# SecureCode config (auto-generated, DO NOT commit)\nSECURECODE_ONBOARDING_TOKEN=${token}\n`, { mode: 0o600 }); } } catch { /* best-effort */ } } function clearOnboardingTokenFromRc(): void { try { const rcPath = join(process.cwd(), '.securecoderc'); if (!existsSync(rcPath)) return; const content = readFileSync(rcPath, 'utf-8'); const lines = content.split('\n').filter(l => !l.trim().startsWith('SECURECODE_ONBOARDING_TOKEN=')); writeFileSync(rcPath, lines.join('\n'), { mode: 0o600 }); } catch { /* best-effort */ } } // Try to restore onboarding token from .securecoderc on startup if (!onboardingToken) { const savedToken = readOnboardingTokenFromRc(); if (savedToken) onboardingToken = savedToken; } // ── Tag-based vault discovery ───────────────────────────────────────── // Fetches unique tag combinations from existing secrets so the user can // select which project/env to use instead of re-importing. interface TagGroup { project: string; env: string; count: number; } async function discoverVaultTags(): Promise<TagGroup[]> { try { const client = getClient(); const secrets = await client.listSecrets(); const tagMap = new Map<string, number>(); for (const s of secrets) { const proj = s.tags?.project || '(untagged)'; const env = s.tags?.env || '(untagged)'; const key = `${proj}::${env}`; tagMap.set(key, (tagMap.get(key) || 0) + 1); } const groups: TagGroup[] = []; for (const [key, count] of tagMap) { const [project, env] = key.split('::'); groups.push({ project, env, count }); } return groups; } catch { return []; } } // Helper: find .mcp.json config path (project-level or global) function findMcpConfigPaths(): { primary: { path: string; isProject: boolean }; conflicts: { path: string; isProject: boolean }[] } | null { const results: { path: string; isProject: boolean }[] = []; // Check project-level const cwd = process.cwd(); const projectConfig = join(cwd, '.mcp.json'); if (existsSync(projectConfig)) { results.push({ path: projectConfig, isProject: true }); } // Check global Claude Code config (~/.claude.json) // Claude Code stores MCP servers in two formats: // 1. Top-level: config.mcpServers.securecode // 2. Per-project: config.projects["C:/path/to/project"].mcpServers.securecode // We need to check ALL projects because path format varies (forward vs backslash on Windows) const globalConfig = join(os.homedir(), '.claude.json'); if (existsSync(globalConfig)) { try { const parsed = JSON.parse(readFileSync(globalConfig, 'utf-8')); let hasSecurecodeEntry = false; // Check top-level mcpServers if (parsed?.mcpServers?.securecode) { hasSecurecodeEntry = true; } // Check ALL project entries (avoids path format mismatch on Windows) if (!hasSecurecodeEntry && parsed?.projects) { for (const projectPath of Object.keys(parsed.projects)) { if (parsed.projects[projectPath]?.mcpServers?.securecode) { hasSecurecodeEntry = true; break; } } } if (hasSecurecodeEntry) { results.push({ path: globalConfig, isProject: false }); } else if (results.length === 0) { // No project config either — global is the target results.push({ path: globalConfig, isProject: false }); } } catch { if (results.length === 0) { results.push({ path: globalConfig, isProject: false }); } } } if (results.length === 0) return null; // Primary = project-level if available, otherwise global const primary = results.find(r => r.isProject) || results[0]; const conflicts = results.filter(r => r !== primary); return { primary, conflicts }; } // Backward-compatible wrapper function findMcpConfigPath(): { path: string; isProject: boolean } | null { const result = findMcpConfigPaths(); return result ? result.primary : null; } // Helper: write MCP config WITHOUT API key in env (key lives in .securecoderc only) function writeMcpConfig(configPath: string): void { let config: Record<string, unknown> = {}; try { if (existsSync(configPath)) { config = JSON.parse(readFileSync(configPath, 'utf-8')); } } catch { // Malformed JSON — start fresh config = {}; } const mcpServers = (config.mcpServers || {}) as Record<string, unknown>; mcpServers.securecode = { command: 'npx', args: ['-y', '@securecode/mcp-server'], }; config.mcpServers = mcpServers; writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); } // Helper: remove API key from env in existing securecode entries (key now lives in .securecoderc only) function cleanMcpConfigEnv(configPath: string): boolean { try { const raw = readFileSync(configPath, 'utf-8'); const config = JSON.parse(raw); // Check top-level mcpServers if (config?.mcpServers?.securecode) { delete config.mcpServers.securecode.env; writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); return true; } // Check project-scoped mcpServers in ~/.claude.json const projects = config?.projects; if (projects) { for (const projectPath of Object.keys(projects)) { const proj = projects[projectPath]; if (proj?.mcpServers?.securecode) { delete proj.mcpServers.securecode.env; writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); return true; } } } } catch { // Best-effort } return false; } server.tool( 'onboard', 'Start or continue the SecureCode onboarding. Guides the user through signup, .env import, API key creation, MCP configuration, and optional SDK setup — all from Claude Code. Call this tool multiple times to progress through the steps.', { action: z.enum(['start', 'configure-auto', 'configure-manual', 'setup-sdk', 'add-environment', 'select-secrets']).optional() .describe('Action to perform. "start" (default) progresses through signup/import steps. "configure-auto" lets the agent write the API key to the MCP config file automatically. "configure-manual" shows the API key so the user can configure it themselves. "setup-sdk" returns SDK installation instructions for the agent to execute. "add-environment" guides through adding secrets for a new environment (e.g., production) without repeating the full onboarding. "select-secrets" lists existing secrets in the vault by tags so the user can select which ones to use (recovery/skip import).'), configPath: z.string().optional() .describe('Path to the MCP config file (.mcp.json or claude.json). Only needed for configure-auto if auto-detection fails.'), selectedProject: z.string().optional() .describe('Project tag selected by user (from select-secrets action). Used to configure .securecoderc.'), selectedEnv: z.string().optional() .describe('Environment tag selected by user (from select-secrets action). Used to configure .securecoderc.'), }, async ({ action, configPath, selectedProject, selectedEnv }) => { try { const effectiveAction = action || 'start'; // ── Select secrets from vault (tag-based recovery) ── if (effectiveAction === 'select-secrets') { if (!hasApiKey) { return wrapResponse([{ type: 'text', text: 'The MCP server does not have an API key configured yet. Complete the onboarding first (run onboard).', }], true); } // If project/env were selected, save them to .securecoderc and confirm if (selectedProject || selectedEnv) { const proj = selectedProject && selectedProject !== '(untagged)' ? selectedProject : null; const env = selectedEnv && selectedEnv !== '(untagged)' ? selectedEnv : null; if (proj) onboardingProject = proj; if (env) onboardingEnv = env; // Update .securecoderc with selected project/env (keep existing API key) const rcPath = join(process.cwd(), '.securecoderc'); if (existsSync(rcPath)) { let content = readFileSync(rcPath, 'utf-8'); // Remove old project/env lines const lines = content.split('\n').filter(l => { const t = l.trim(); return !t.startsWith('SECURECODE_PROJECT=') && !t.startsWith('SECURECODE_ENV='); }); if (proj) lines.push(`SECURECODE_PROJECT=${proj}`); if (env) lines.push(`SECURECODE_ENV=${env}`); writeFileSync(rcPath, lines.filter(l => l !== '').join('\n') + '\n', { mode: 0o600 }); } // List actual secrets for confirmation const client = getClient(); const secrets = await client.listSecrets(); const filtered = secrets.filter(s => { if (proj && s.tags?.project !== proj) return false; if (env && s.tags?.env !== env) return false; return true; }); return wrapResponse([{ type: 'text', text: [ `✓ Configuration updated: project=${proj || '(all)'}, env=${env || '(all)'}`, '', `Found ${filtered.length} secrets matching this selection:`, ...filtered.map(s => ` - ${s.name}`), '', 'These secrets are already in your vault and will be loaded by the SDK.', '.securecoderc has been updated with the project/env configuration.', '', 'Next: Set up the SDK to load these secrets at runtime.', 'AGENT INSTRUCTION: Call onboard with action "setup-sdk" to configure the SDK.', ].join('\n'), }]); } // No selection yet — list secrets grouped by tags let allSecrets: Array<{ name: string; tags: Record<string, string> }> = []; try { const client = getClient(); const secrets = await client.listSecrets(); allSecrets = secrets.map(s => ({ name: s.name, tags: s.tags || {} })); } catch { return wrapResponse([{ type: 'text', text: 'Could not fetch secrets from the vault. Try again or run onboard(action: "start") to re-import.', }], true); } if (allSecrets.length === 0) { return wrapResponse([{ type: 'text', text: [ 'No secrets found in your vault.', '', 'You need to import secrets first. Run onboard (action: "start") to begin the import flow.', ].join('\n'), }]); } // Group secrets by project/env const grouped = new Map<string, string[]>(); for (const s of allSecrets) { const proj = s.tags.project || '(untagged)'; const env = s.tags.env || '(untagged)'; const key = `${proj}::${env}`; if (!grouped.has(key)) grouped.set(key, []); grouped.get(key)!.push(s.name); } const sections: string[] = []; for (const [key, names] of grouped) { const [proj, env] = key.split('::'); sections.push(`### Project: ${proj}, Env: ${env} (${names.length} secrets)`); sections.push(...names.map(n => ` - [x] ${n}`)); sections.push(''); } return wrapResponse([{ type: 'text', text: [ '## Your Secrets in SecureCode', '', ...sections, 'AGENT INSTRUCTION:', 'Present this list to the user with ALL secrets selected by default (shown as checkboxes above).', 'The user can deselect any they don\'t want for this project.', 'After confirmation, call onboard with action: "select-secrets", selectedProject: "<project>", selectedEnv: "<env>"', 'using the project/env from the group the user confirmed.', '', 'If there are multiple groups, let the user pick which group(s) to use.', 'If the user wants to import NEW secrets instead, call onboard with action: "start".', ].join('\n'), }]); } // ── Configure actions (after import is completed) ── if (effectiveAction === 'configure-auto' || effectiveAction === 'configure-manual') { if (!onboardingToken) { return wrapResponse([{ type: 'text', text: 'No active onboarding session. Call onboard first to start the process.', }], true); } // Claim the API key (single-use) const claimRes = await onboardingFetch<Record<string, string>>( `/onboarding/${onboardingToken}/claim-key`, { method: 'POST' }, ); if ((claimRes as Record<string, unknown>).error) { const errMsg = (claimRes as Record<string, unknown>).error as string; // Key already claimed — check if it was auto-configured if (errMsg.includes('already claimed')) { onboardingToken = null; clearOnboardingTokenFromRc(); return wrapResponse([{ type: 'text', text: [ '✓ API key was already created for this onboarding session.', '', 'If you need to reconfigure, create a new API key from the SecureCode dashboard:', `${baseUrl}/en/settings`, ].join('\n'), }]); } return wrapResponse([{ type: 'text', text: `Error: ${errMsg}` }], true); } const { apiKey: newKey, keyName } = claimRes; if (effectiveAction === 'configure-manual') { // User wants to see the key and do it themselves onboardingToken = null; clearOnboardingTokenFromRc(); return wrapResponse([{ type: 'text', text: [ `✓ API key "${keyName}" created.`, '', `API Key: ${newKey}`, '', 'Add it to your MCP config file (.mcp.json or ~/.claude.json):', '```json', '{', ' "mcpServers": {', ' "securecode": {', ' "command": "npx",', ' "args": ["-y", "@securecode/mcp-server"]', ' }', ' }', '}', '```', '', 'Create a .securecoderc file in your project root with:', '```', `SECURECODE_API_KEY=${newKey}`, '```', 'Add .securecoderc to your .gitignore.', 'The MCP server reads the API key from .securecoderc automatically.', '', 'After saving, close and reopen Claude Code to activate SecureCode.', '', '--- What\'s next? ---', '', 'Right now your app still reads secrets from .env files.', 'With the SecureCode SDK, your app can load secrets directly from SecureCode at runtime.', '', 'What you get:', ' - Every access audited (who, when, from where) — visible in the dashboard', ' - Security rules and email notifications on sensitive access', ' - Secrets encrypted and under your control', ' - No more .env files scattered across machines', '', 'TELL THE USER: After reopening Claude Code, say "configure the SecureCode SDK in this project"', 'and I\'ll show you the options and set it up for you.', '', 'AGENT INSTRUCTION: When the user asks to configure the SDK, call the onboard tool', 'with action "setup-sdk". This will list the user\'s secrets and return the setup options.', ].join('\n'), }]); } // Auto-configure: write the key to the config file const detected = configPath ? { primary: { path: configPath, isProject: configPath.includes('.mcp.json') }, conflicts: [] } : findMcpConfigPaths(); if (!detected) { // No config file found — return key for manual setup onboardingToken = null; clearOnboardingTokenFromRc(); return wrapResponse([{ type: 'text', text: [ `✓ API key "${keyName}" created.`, '', 'Could not find an MCP config file (.mcp.json or ~/.claude.json).', `The API key is: ${newKey}`, '', 'Create a .mcp.json file in your project root or ~/.claude.json with:', '```json', '{', ' "mcpServers": {', ' "securecode": {', ' "command": "npx",', ' "args": ["-y", "@securecode/mcp-server"]', ' }', ' }', '}', '```', '', `Then create a .securecoderc file in your project root with:`, `SECURECODE_API_KEY=${newKey}`, '', 'The MCP server reads the API key from .securecoderc automatically.', ].join('\n'), }]); } // Write the config to primary location (no API key in config — it lives in .securecoderc) writeMcpConfig(detected.primary.path); const configLabel = detected.primary.isProject ? 'project (.mcp.json)' : 'global (~/.claude.json)'; // Write .securecoderc and update .gitignore writeSecurecodeRc(newKey, onboardingProject, onboardingEnv); updateGitignore(); // Resolve conflicts: remove API key from env in other config files // (key now lives only in .securecoderc) const conflictsFixed: string[] = []; for (const conflict of detected.conflicts) { if (cleanMcpConfigEnv(conflict.path)) { conflictsFixed.push(conflict.path); } } // Also clean ~/.claude.json global config directly. // When user runs `claude mcp add securecode` before onboarding, it creates an entry // with env:{} — remove API key from there (it lives in .securecoderc only). try { const globalConfig = join(os.homedir(), '.claude.json'); if (existsSync(globalConfig)) { const raw = readFileSync(globalConfig, 'utf-8'); const parsed = JSON.parse(raw); let modified = false; // Clean top-level mcpServers entry if (parsed?.mcpServers?.securecode?.env) { delete parsed.mcpServers.securecode.env; modified = true; } // Clean per-project entries (path format varies on Windows) if (parsed?.projects && typeof parsed.projects === 'object') { for (const [, proj] of Object.entries(parsed.projects)) { const p = proj as Record<string, unknown>; const mcpServers = p?.mcpServers as Record<string, Record<string, unknown>> | undefined; if (mcpServers?.securecode?.env) { delete mcpServers.securecode.env; modified = true; } } } if (modified) { writeFileSync(globalConfig, JSON.stringify(parsed, null, 2) + '\n'); if (!conflictsFixed.includes(globalConfig)) conflictsFixed.push(globalConfig); } } } catch { /* best-effort */ } onboardingToken = null; clearOnboardingTokenFromRc(); return wrapResponse([{ type: 'text', text: [ '✓ Onboarding complete!', '', `API key "${keyName}" created and configured automatically.`, `Config written to: ${detected.primary.path} (${configLabel})`, 'Created .securecoderc with API key and project config (added to .gitignore)', ...(conflictsFixed.length > 0 ? [`Also cleaned API key from env in: ${conflictsFixed.join(', ')} (key is now only in .securecoderc)`] : []), '', '⚡ NEXT STEP: Close and reopen Claude Code to activate SecureCode.', '', 'After reopening, your secrets are ready to use from Claude Code.', '', '--- What\'s next? ---', '', 'Right now your app still reads secrets from .env files.', 'With the SecureCode SDK, your app can load secrets directly from SecureCode at runtime.', '', 'What you get:', ' - Every access audited (who, when, from where) — visible in the dashboard', ' - Security rules and email notifications on sensitive access', ' - Secrets encrypted and under your control', ' - No more .env files scattered across machines', '', 'TELL THE USER: After reopening Claude Code, say "configure the SecureCode SDK in this project"', 'and I\'ll show you the options and set it up for you.', '', 'AGENT INSTRUCTION: When the user asks to configure the SDK, call the onboard tool', 'with action "setup-sdk". This will list the user\'s secrets and return the setup options.', 'Present the options to the user, let them choose, then execute the steps.', '', 'IMPORTANT — MCP CONFIG TROUBLESHOOTING:', 'The API key is stored ONLY in .securecoderc (never in MCP config env).', 'If after restart the MCP gives "NO_API_KEY" error:', ' 1. Check .securecoderc exists in the project root with SECURECODE_API_KEY=sc_...', ' 2. Make sure .securecoderc is in the same directory where Claude Code is opened', ' 3. If .securecoderc is missing, the user must run the onboarding again', ].join('\n'), }]); } // ── Add environment action (after onboarding is complete) ── if (effectiveAction === 'add-environment') { if (!hasApiKey) { return wrapResponse([{ type: 'text', text: 'The MCP server does not have an API key configured yet. Complete the onboarding first (run onboard), then close and reopen Claude Code.', }], true); } // Get the base URL for the dashboard const baseUrl = (process.env.SECURECODE_API_URL || 'https://securecodehq.com').replace(/\/api$/, '').replace(/\/$/, ''); return wrapResponse([{ type: 'text', text: [ '## Add New Environment', '', 'To add secrets for a new environment (e.g., production, staging, testing):', '', `1. Open the SecureCode import page: ${baseUrl}/import`, '', '2. Select your project and choose the new environment', ' - Use the "Other..." option if your environment is not listed', '', '3. Import your .env file for that environment', '', '4. Update your local .securecoderc if needed:', ' ```', ' SECURECODE_ENV=production', ' ```', '', '5. Or use different SECURECODE_ENV values per deployment in your hosting platform', '', 'Your existing secrets remain untouched. The new environment\'s secrets are tagged separately.', '', 'AGENT INSTRUCTIONS:', '- Open the import URL in the browser for the user', '- After they confirm the import is done, ask if they want to update .securecoderc', '- If yes, update the SECURECODE_ENV value in .securecoderc', '- Tell the user that loadEnv() will automatically filter by the configured environment', ].join('\n'), }]); } // ── Setup SDK action (after MCP is configured and reconnected) ── if (effectiveAction === 'setup-sdk') { if (!hasApiKey) { return wrapResponse([{ type: 'text', text: 'The MCP server does not have an API key configured yet. Complete the onboarding first (run onboard), then close and reopen Claude Code before setting up the SDK.', }], true); } // Fetch user's secrets to provide context let secretNames: string[] = []; let secretTags: Record<string, string>[] = []; try { const client = getClient(); const secrets = await client.listSecrets(); secretNames = secrets.map(s => s.name); secretTags = secrets.map(s => s.tags); } catch { // Best-effort — continue without secret list } // Detect project/env from secret tags const allTags = secretTags.reduce((acc, t) => ({ ...acc, ...t }), {} as Record<string, string>); const projectName = allTags.project || 'unknown'; const envName = allTags.env || 'unknown'; const secretListStr = secretNames.length > 0 ? secretNames.map(n => ` - ${n}`).join('\n') : ' (no secrets found — import some first)'; return wrapResponse([{ type: 'text', text: [ '## SecureCode SDK Setup', '', `The user has ${secretNames.length} secrets in SecureCode (project: ${projectName}, env: ${envName}):`, secretListStr, '', 'Present these options to the user and let them choose:', '', '---', '', '### Option 1: loadEnv() — Load all secrets at startup (recommended)', '', 'One line at your app\'s entry point loads ALL secrets into process.env.', 'Your existing code doesn\'t change at all — process.env.DATABASE_URL etc. keep working.', '', 'Best for: Most projects. Zero code changes beyond the setup.', '', '### Option 2: getSecret() — Access individual secrets on demand', '', 'Fetch specific secrets when you need them, with full control over timing.', 'Requires changing each process.env.X reference to await getSecret("X").', '', 'Best for: Projects that only need 1-2 secrets, or when you want lazy loading.', '', '---', '', 'AGENT INSTRUCTIONS — You MUST follow these steps exactly. Do NOT skip any step.', 'Do NOT tell the user "it\'s already configured" without verifying the actual code.', '', '## For Option 1 (loadEnv):', '', 'Step 1: Install the SDK if not already installed:', '```bash', 'npm install @securecode/sdk', '```', '', 'Step 2: You MUST write code in the app\'s entry point. Find the right file:', '- Next.js: src/instrumentation.ts (create if it doesn\'t exist)', '- Express/Fastify: Top of server.ts/app.ts, before database connections', '- Plain Node.js: Top of index.ts/main.ts', '', 'You MUST add this code. Read the file first, then edit it:', '', 'For Next.js (instrumentation.ts):', '```typescript', 'export async function register() {', ' if (process.env.NEXT_RUNTIME === "nodejs") {', ' const { loadEnv } = await import("@securecode/sdk");', ' await loadEnv({ override: true });', ' }', '}', '```', '', 'IMPORTANT: The NEXT_RUNTIME guard and dynamic import() are required because', 'instrumentation.ts is compiled for both Node.js and Edge runtimes.', 'The SDK uses Node.js built-ins (fs, os, path) that break the Edge compilation.', 'A top-level static import will cause webpack errors.', '', 'NOTE: In Next.js 14, instrumentation requires `experimental: { instrumentationHook: true }`', 'in next.config.js. Next.js 15+ enables it by default. If the project uses Next.js 14,', 'check next.config and add the flag if missing.',