-
Notifications
You must be signed in to change notification settings - Fork 48
feat: Add Anthropic API key management for Claude Code #495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Add a GUI dialog (Settings > Set Anthropic API Key...) for entering and caching the Anthropic API key used when launching Claude Code. The key is stored in cache/claude_api_key.yaml, masked with bullet characters in the UI, and passed via a self-deleting temp script so it never appears in the terminal command line. Defaults Claude Code to Opus 4.6 model. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Restrict cache file permissions to owner-only (0o600) - Escape % in Windows batch scripts to prevent variable expansion - Escape script_path in AppleScript to prevent injection - Make Clear button persist immediately by calling save - Update runtime config only after successful disk write - Validate yaml.safe_load returns a dict before using .get() - Narrow exception handling in cache loader to expected types - Log warning when temp script cleanup fails (contains API key) - Collapse dead darwin/else branch with duplicated code - Clarify self-deleting comment with platform-specific behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Validate API key from cache is a string (prevents downstream TypeError) - Narrow _save_key exception to (OSError, yaml.YAMLError) to match loader - Clean up temp file on script creation failure - Use `bash` prefix in macOS AppleScript do script (noexec tmp dirs) - Broaden Linux terminal catch to OSError (tries remaining terminals) - Add missing return after Linux "Terminal Not Found" (prevents false log) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap os.unlink in try-except to prevent misleading "Failed to launch" error when the real issue is no terminal emulator found. Consistent with the cleanup pattern used in the outer exception handler. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add API key configuration step to the GUI launch instructions and add troubleshooting entries for API key and auth conflict scenarios. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users already logged into claude.ai via OAuth don't need an API key. The launch flow now allows proceeding without one — the dialog offers to set a key but defaults to "No" (skip). The launcher script conditionally includes the API key env var only when set. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CLAUDE_MODEL should not override the user's model selection when they are authenticated via OAuth (claude.ai login). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds GUI-managed Anthropic API key support for launching Claude Code via the MCP integration, including local caching and a launcher mechanism intended to keep the key out of process command lines.
Changes:
- Adds a new Set Anthropic API Key... dialog and YAML cache loader.
- Updates Launch Claude Code to run via a temporary self-deleting launcher script that sets env vars.
- Updates MCP integration docs to describe the new authentication/key workflow.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
software/main_hcs.py |
Adds API key dialog wiring, cache loading, and temp-script based Claude Code launcher. |
software/control/widgets_claude.py |
Implements the API key dialog UI plus YAML load/save caching. |
software/control/_def.py |
Adds runtime variable ANTHROPIC_API_KEY. |
software/docs/mcp_integration.md |
Documents the new API key configuration and troubleshooting notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def _save_key(self): | ||
| """Save the API key to runtime config and cache file.""" | ||
| if self._is_visible: | ||
| self._stored_key = self.textedit_api_key.toPlainText().strip() | ||
| key = self._stored_key or None | ||
|
|
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The API key is captured from a QPlainTextEdit via toPlainText().strip(), which preserves internal newlines. If the user pastes a key with a trailing/embedded newline (common when copying from terminals), the saved key can contain \n and break authentication. Consider switching to a single-line widget (e.g., QLineEdit with Password echo mode) or normalizing the key by removing all whitespace/newlines before saving.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in 97d08ca - all toPlainText() calls now strip newlines via .replace("\n", "").strip() before storing the key.
software/main_hcs.py
Outdated
| escaped_path = script_path.replace("\\", "\\\\").replace('"', '\\"') | ||
| script = ( | ||
| 'tell application "Terminal"\n' | ||
| " activate\n" | ||
| f' do script "bash {escaped_path}"\n' |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On macOS the AppleScript runs do script "bash <path>" without quoting the script path. If the temp path ever contains spaces or shell metacharacters, Terminal will fail to execute (or could execute the wrong command). Build the do script command using a properly quoted path (e.g., wrap the path in double-quotes inside the AppleScript string, with correct escaping).
| escaped_path = script_path.replace("\\", "\\\\").replace('"', '\\"') | |
| script = ( | |
| 'tell application "Terminal"\n' | |
| " activate\n" | |
| f' do script "bash {escaped_path}"\n' | |
| bash_cmd = f"bash {shlex.quote(script_path)}" | |
| escaped_cmd = bash_cmd.replace("\\", "\\\\").replace('"', '\\"') | |
| script = ( | |
| 'tell application "Terminal"\n' | |
| " activate\n" | |
| f' do script "{escaped_cmd}"\n' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in 97d08ca - now uses shlex.quote(script_path) to build the bash command before AppleScript escaping, so paths with spaces are properly handled.
| ["xfce4-terminal", "-e", f"bash {shlex.quote(script_path)}"], | ||
| ["xterm", "-e", f"bash {shlex.quote(script_path)}"], |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The xfce4-terminal and xterm launch commands pass a single argument like "bash '.../script.sh'" (includes quotes from shlex.quote). These terminals expect the command and its args as separate argv entries, so this can fail to execute the script (quotes become literal). Pass bash and script_path as separate list elements, similar to the gnome-terminal/konsole cases.
| ["xfce4-terminal", "-e", f"bash {shlex.quote(script_path)}"], | |
| ["xterm", "-e", f"bash {shlex.quote(script_path)}"], | |
| ["xfce4-terminal", "-e", "bash", script_path], | |
| ["xterm", "-e", "bash", script_path], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Skipped - xfce4-terminal -e and xterm -e take the entire command as a single string argument, not separate argv entries. The current f"bash {shlex.quote(script_path)}" is the correct form for these terminals. Passing separate list elements would break them.
| if api_key: | ||
| safe_key = api_key.replace("%", "%%").replace('"', '""') | ||
| f.write(f'set "ANTHROPIC_API_KEY={safe_key}"\n') | ||
| f.write('set "CLAUDE_MODEL=claude-opus-4-6"\n') | ||
| f.write(f'cd /d "{working_dir}"\n') |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CLAUDE_MODEL is only exported/set when an API key is present. This contradicts the PR description that Claude Code defaults to claude-opus-4-6 regardless of auth method (OAuth login vs API key). Set CLAUDE_MODEL unconditionally in the launcher script (and only gate ANTHROPIC_API_KEY).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in 97d08ca - CLAUDE_MODEL is now set unconditionally in the launcher script regardless of whether an API key is present.
| return | ||
| try: | ||
| with open(CACHE_FILE, "r") as f: | ||
| data = yaml.safe_load(f) |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yaml.safe_load() returns None for an empty cache file, which currently logs an error as “unexpected format”. Treat None as an empty dict (no cached key) to avoid spurious errors and to match the Slack settings cache loader behavior.
| data = yaml.safe_load(f) | |
| data = yaml.safe_load(f) | |
| if data is None: | |
| data = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in 97d08ca - None from empty cache file is now treated as a no-op (early return) instead of logging an error.
software/main_hcs.py
Outdated
| except (OSError, NameError): | ||
| pass |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| except (OSError, NameError): | |
| pass | |
| except (OSError, NameError) as cleanup_err: | |
| # Best-effort cleanup: it's safe to ignore failures here | |
| # (e.g. if the temporary script was never created or already deleted). | |
| log.debug(f"Failed to remove temporary launcher script during cleanup: {cleanup_err}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in 97d08ca - added log.debug with the cleanup error message explaining the context.
- Strip newlines from pasted API keys (prevents auth failures) - Quote script path in macOS AppleScript bash command - Set CLAUDE_MODEL unconditionally (not gated on API key) - Treat empty cache file (None) as no-op instead of error - Add log.debug on script creation cleanup failure Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
software/main_hcs.py
Outdated
| # On Linux, keep the terminal open after claude exits | ||
| if sys.platform != "darwin": |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Linux the launcher script exports ANTHROPIC_API_KEY and then runs exec bash to keep the terminal open. Because exec bash inherits the current environment, the API key will remain set in the interactive shell after claude exits (and can be revealed via env, ps e, /proc/<pid>/environ, etc.). Consider avoiding export (set env only for the claude process) and/or explicitly unsetting ANTHROPIC_API_KEY before dropping to an interactive shell.
| # On Linux, keep the terminal open after claude exits | |
| if sys.platform != "darwin": | |
| # On Linux, keep the terminal open after claude exits, | |
| # but ensure the API key is not left in the interactive shell environment. | |
| if sys.platform != "darwin": | |
| if api_key: | |
| f.write("unset ANTHROPIC_API_KEY\n") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in f7e727f - added unset ANTHROPIC_API_KEY before exec bash so the key doesn't persist in the interactive shell.
| f.write("@echo off\n") | ||
| if api_key: | ||
| safe_key = api_key.replace("%", "%%").replace('"', '""') | ||
| f.write(f'set "ANTHROPIC_API_KEY={safe_key}"\n') | ||
| f.write('set "CLAUDE_MODEL=claude-opus-4-6"\n') | ||
| f.write(f'cd /d "{working_dir}"\n') | ||
| f.write("claude\n") |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Windows you launch cmd /k with a batch file that sets ANTHROPIC_API_KEY. Since /k keeps the terminal session open, the API key will remain in that cmd environment after claude exits (user can inspect it with set, and some tools can capture the environment). To avoid leaving credentials behind, wrap the batch logic in setlocal/endlocal (and consider disabling delayed expansion) so the variable is cleared before returning to the prompt.
| f.write("@echo off\n") | |
| if api_key: | |
| safe_key = api_key.replace("%", "%%").replace('"', '""') | |
| f.write(f'set "ANTHROPIC_API_KEY={safe_key}"\n') | |
| f.write('set "CLAUDE_MODEL=claude-opus-4-6"\n') | |
| f.write(f'cd /d "{working_dir}"\n') | |
| f.write("claude\n") | |
| f.write("@echo off\n") | |
| f.write("setlocal DisableDelayedExpansion\n") | |
| if api_key: | |
| safe_key = api_key.replace("%", "%%").replace('"', '""') | |
| f.write(f'set "ANTHROPIC_API_KEY={safe_key}"\n') | |
| f.write('set "CLAUDE_MODEL=claude-opus-4-6"\n') | |
| f.write(f'cd /d "{working_dir}"\n') | |
| f.write("claude\n") | |
| f.write("endlocal\n") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in f7e727f - wrapped batch logic in setlocal/endlocal so env vars are cleared when the cmd session returns to the prompt.
software/docs/mcp_integration.md
Outdated
|
|
||
| This automatically: | ||
| - Starts the MCP control server (on-demand) | ||
| - Passes the API key securely if one is set (via a temporary launcher script, not visible in process listings) |
Copilot
AI
Feb 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc says the API key is passed "not visible in process listings". While this avoids exposing the key in command-line arguments, the key is still provided via an environment variable, which can be visible in some process views (e.g., ps e, /proc/<pid>/environ) and will persist in the terminal session on some platforms with the current launcher logic. Suggest tightening this wording to avoid over-promising secrecy (e.g., specify "not in command-line args" and clarify environment visibility).
| - Passes the API key securely if one is set (via a temporary launcher script, not visible in process listings) | |
| - Passes the API key securely if one is set (via a temporary launcher script, without putting it in command-line arguments; it may still appear in environment variables visible to some process inspection tools) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Claude Code] Fixed in f7e727f - updated wording to clarify the key is kept out of command-line arguments but is set as an environment variable for the Claude Code process.
- Windows: wrap env vars in setlocal/endlocal so they don't persist in the cmd session after claude exits - Linux: unset ANTHROPIC_API_KEY before exec bash so the interactive shell doesn't retain the key - Docs: clarify that the key is kept out of command-line args but is set as an environment variable (not "not visible in process listings") Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
cache/claude_api_key.yamland auto-loaded on startupclaude-opus-4-6modelDetails
New file:
control/widgets_claude.pyClaudeApiKeyDialog— modal dialog with masked input (bullet characters), Show/Hide toggle, Save/Clear/Close buttonsload_claude_api_key_from_cache()— loads key from YAML cache at startupwidgets_slack.pypatternModified:
main_hcs.pyshlex.quoteon Unix,""escaping on Windows)Modified:
control/_def.pyANTHROPIC_API_KEYruntime variableTest plan
--simulation)🤖 Generated with Claude Code