feat: add Claude Code as a CLI provider with published images#120
feat: add Claude Code as a CLI provider with published images#120GordonBeeming wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request integrates Claude Code as a switchable CLI provider alongside GitHub Copilot, introducing new authentication, model, and tool providers, along with corresponding Dockerfile variants, default airlock rules, and unit tests. The review feedback highlights three key issues: a security vulnerability where sensitive credentials are written before file permissions are restricted, a potential process hang when synchronously reading macOS Keychain credentials, and a bug in image tag resolution that could lead to double-prefixing.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (!File.Exists(path) || File.ReadAllText(path) != json) | ||
| { | ||
| File.WriteAllText(path, json); | ||
| TryRestrictPermissions(path); | ||
| DebugLogger.Log("Refreshed ~/.claude/.credentials.json from the macOS Keychain"); | ||
| } |
There was a problem hiding this comment.
Writing sensitive credentials (OAuth refresh tokens) to a file with default permissions before restricting them creates a brief window where other local users could potentially read the file. To prevent this security risk, create the file empty first, restrict its permissions immediately, and then write the sensitive content.
bool exists = File.Exists(path);
if (!exists)
{
using (File.Create(path)) {}
TryRestrictPermissions(path);
}
if (!exists || File.ReadAllText(path) != json)
{
File.WriteAllText(path, json);
if (exists)
{
TryRestrictPermissions(path);
}
DebugLogger.Log("Refreshed ~/.claude/.credentials.json from the macOS Keychain");
}| var stdout = process.StandardOutput.ReadToEnd(); | ||
| process.StandardError.ReadToEnd(); | ||
| process.WaitForExit(); | ||
|
|
||
| return process.ExitCode == 0 ? stdout.Trim() : null; |
There was a problem hiding this comment.
Calling process.StandardOutput.ReadToEnd() synchronously before process.WaitForExit() can cause the CLI tool to hang indefinitely if the process blocks (e.g., if the macOS Keychain is locked or prompts the user with a GUI dialog for authorization). To prevent this, use process.WaitForExit(timeout) with a timeout first, and only read from the stream if the process exited successfully, killing it otherwise.
if (process.WaitForExit(5000))
{
var stdout = process.StandardOutput.ReadToEnd();
return process.ExitCode == 0 ? stdout.Trim() : null;
}
else
{
try { process.Kill(); } catch {}
DebugLogger.Log("Keychain read timed out");
return null;
}| // Image name format: ghcr.io/gordonbeeming/copilot_here:claude-{variant} | ||
| // Tag matches what users invoke: "claude-dotnet", "claude-latest", etc. | ||
| const string imagePrefix = "ghcr.io/gordonbeeming/copilot_here"; | ||
| var imageTag = string.IsNullOrEmpty(tag) ? "claude-latest" : $"claude-{tag}"; |
There was a problem hiding this comment.
If the tag parameter already starts with claude- (e.g., if a user manually configures or passes it), prepending claude- again will result in a double prefix like claude-claude-dotnet. Add a check to prevent double-prefixing.
var imageTag = string.IsNullOrEmpty(tag)
? "claude-latest"
: tag.StartsWith("claude-", StringComparison.OrdinalIgnoreCase)
? tag
: $"claude-{tag}";ea093d4 to
50d4ae1
Compare
Register Claude Code (@anthropic-ai/claude-code) as a selectable provider alongside GitHub Copilot, with its own claude-* Docker image family. - ClaudeCodeTool/ClaudeAuthProvider/ClaudeModelProvider + ToolRegistry entry - Auth reuses the host login: mounts ~/.claude and ~/.claude.json, and on macOS re-seeds ~/.claude/.credentials.json from the Keychain each run so the token stays valid (the host rotates it). ANTHROPIC_API_KEY and a setup-token CLAUDE_CODE_OAUTH_TOKEN take precedence. - Claude manages its own model selection, so the Copilot-oriented model.conf is not forced on it. - Airlock default rules are now per-tool; Claude allows the Anthropic hosts. - claude-cli snippet, 11 claude-* images.json entries and generated Dockerfiles. - publish.yml builds and tags the claude-* family (claude-latest is the default). - README/docs and unit tests. Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitButler <gitbutler@gitbutler.com>
c4542a8 to
96ba8f8
Compare
Summary
Adds Claude Code (
@anthropic-ai/claude-code) as a selectable provider next to GitHub Copilot, with its ownclaude-*Docker image family. Switch with--set-tool claude; the variant you pick (--dotnet,--rust, …) stays the same and the active provider chooses the matching image family.What's included
ClaudeCodeTool/ClaudeAuthProvider/ClaudeModelProvider, registered asclaudeinToolRegistry; newAppPaths.ClaudeConfigPath.~/.claudeand~/.claude.json, and on macOS re-seeds~/.claude/.credentials.jsonfrom the Keychain on every run (the host rotates the refresh token, so a one-time seed goes stale and 401s).ANTHROPIC_API_KEYand aclaude setup-tokenCLAUDE_CODE_OAUTH_TOKENtake precedence./model, so the Copilot-orientedmodel.confis not forced on it. A per-run--model <id>still applies (newManagesOwnModelSelection).GetDefaultRules()follows the active tool, Claude's rules are embedded, and Claude allowsapi.anthropic.com,console.anthropic.com, andstatsig.anthropic.com.claude-clisnippet, 11claude-*entries inimages.json, regenerated Dockerfiles, anddocker/tools/claude/airlock + broker rules.publish.ymlfetches the claude-code version and builds/tags theclaude-*family (claude-latestis the default).docs/updates and unit tests (617 passing).Test plan
dotnet test— 617/617 pass.pwsh docker/generate-dockerfiles.ps1— generatedclaude-*Dockerfiles matchimages.json, no diff on existing copilot entries.claude-defaultlocally and ranclaudeend to end under airlock in this repo: authenticated with the re-seeded Keychain token, used the model from the mounted config, and replied to a prompt (no 403, no 401).Caveats
claudere-login.ANTHROPIC_API_KEYsidesteps this.--edit-airlock-rules.~/.claude.jsonbrings host-specific config in, so/doctormay flag host install paths or a custom statusline. These are cosmetic and don't affect auth or prompts.🤖 Generated with Claude Code