Skip to content

Conversation

@hongquanli
Copy link
Contributor

Summary

  • Adds a Set Anthropic API Key... menu item (Settings menu) so users can enter their Anthropic API key directly in the GUI
  • Key is cached locally in cache/claude_api_key.yaml and auto-loaded on startup
  • Launch Claude Code now passes the key via a self-deleting temp script, so it never appears in the terminal command line or process listings
  • Defaults Claude Code to the claude-opus-4-6 model

Details

New file: control/widgets_claude.py

  • ClaudeApiKeyDialog — modal dialog with masked input (bullet characters), Show/Hide toggle, Save/Clear/Close buttons
  • load_claude_api_key_from_cache() — loads key from YAML cache at startup
  • Follows the existing widgets_slack.py pattern

Modified: main_hcs.py

  • Wires up the API key dialog and cache loading
  • Launch Claude Code now prompts for API key if not set
  • Uses platform-specific self-deleting temp scripts (.sh/.bat) with proper escaping (shlex.quote on Unix, "" escaping on Windows)

Modified: control/_def.py

  • Adds ANTHROPIC_API_KEY runtime variable

Test plan

  • Launch in simulation mode (--simulation)
  • Verify "Set Anthropic API Key..." appears in Settings menu
  • Enter key, save, close, reopen — key persists from cache
  • Click "Launch Claude Code" — terminal opens with key set (not visible in command line)
  • Click "Launch Claude Code" without key — prompts to set one
  • Restart app — key loads from cache automatically
  • Verify Claude Code defaults to Opus 4.6 model

🤖 Generated with Claude Code

hongquanli and others added 7 commits February 8, 2026 19:17
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>
Copy link
Contributor

Copilot AI left a 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.

Comment on lines 150 to 155
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

Copy link

Copilot AI Feb 9, 2026

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

Comment on lines 322 to 326
escaped_path = script_path.replace("\\", "\\\\").replace('"', '\\"')
script = (
'tell application "Terminal"\n'
" activate\n"
f' do script "bash {escaped_path}"\n'
Copy link

Copilot AI Feb 9, 2026

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).

Suggested change
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'

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

Comment on lines +338 to +339
["xfce4-terminal", "-e", f"bash {shlex.quote(script_path)}"],
["xterm", "-e", f"bash {shlex.quote(script_path)}"],
Copy link

Copilot AI Feb 9, 2026

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.

Suggested change
["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],

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

Comment on lines 286 to 290
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')
Copy link

Copilot AI Feb 9, 2026

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).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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)
Copy link

Copilot AI Feb 9, 2026

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.

Suggested change
data = yaml.safe_load(f)
data = yaml.safe_load(f)
if data is None:
data = {}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

Comment on lines 310 to 311
except (OSError, NameError):
pass
Copy link

Copilot AI Feb 9, 2026

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.

Suggested change
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}")

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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>
Copy link
Contributor

Copilot AI left a 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.

Comment on lines 303 to 304
# On Linux, keep the terminal open after claude exits
if sys.platform != "darwin":
Copy link

Copilot AI Feb 9, 2026

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.

Suggested change
# 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")

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.

Comment on lines 285 to 291
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")
Copy link

Copilot AI Feb 9, 2026

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.

Suggested change
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")

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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.


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)
Copy link

Copilot AI Feb 9, 2026

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).

Suggested change
- 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)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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>
Copy link
Contributor

Copilot AI left a 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.

@hongquanli hongquanli merged commit e5e07d2 into master Feb 9, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant