Skip to content

feat: add Claude Code as a CLI provider with published images#120

Draft
GordonBeeming wants to merge 2 commits into
mainfrom
gb/add-claude-provider
Draft

feat: add Claude Code as a CLI provider with published images#120
GordonBeeming wants to merge 2 commits into
mainfrom
gb/add-claude-provider

Conversation

@GordonBeeming

Copy link
Copy Markdown
Owner

Summary

Adds Claude Code (@anthropic-ai/claude-code) as a selectable provider next to GitHub Copilot, with its own claude-* 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

  • App: ClaudeCodeTool / ClaudeAuthProvider / ClaudeModelProvider, registered as claude in ToolRegistry; new AppPaths.ClaudeConfigPath.
  • Auth: reuses your host login. Mounts ~/.claude and ~/.claude.json, and on macOS re-seeds ~/.claude/.credentials.json from the Keychain on every run (the host rotates the refresh token, so a one-time seed goes stale and 401s). ANTHROPIC_API_KEY and a claude setup-token CLAUDE_CODE_OAUTH_TOKEN take precedence.
  • Model: Claude manages its own model via its config and /model, so the Copilot-oriented model.conf is not forced on it. A per-run --model <id> still applies (new ManagesOwnModelSelection).
  • Airlock: default rules are now per-tool. GetDefaultRules() follows the active tool, Claude's rules are embedded, and Claude allows api.anthropic.com, console.anthropic.com, and statsig.anthropic.com.
  • Docker: claude-cli snippet, 11 claude-* entries in images.json, regenerated Dockerfiles, and docker/tools/claude/ airlock + broker rules.
  • CI: publish.yml fetches the claude-code version and builds/tags the claude-* family (claude-latest is the default).
  • Docs/tests: README + docs/ updates and unit tests (617 passing).

Test plan

  • dotnet test — 617/617 pass.
  • pwsh docker/generate-dockerfiles.ps1 — generated claude-* Dockerfiles match images.json, no diff on existing copilot entries.
  • Built claude-default locally and ran claude end 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

  • Because the sandbox and host share one login, a refresh triggered inside a long sandbox session rotates the token and can leave the host Keychain copy stale, so the host Claude occasionally needs a one-off claude re-login. ANTHROPIC_API_KEY sidesteps this.
  • The Claude airlock rules are a starting point; MCP servers or other hosts may need --edit-airlock-rules.
  • Mounting the full ~/.claude.json brings host-specific config in, so /doctor may flag host install paths or a custom statusline. These are cosmetic and don't affect auth or prompts.

🤖 Generated with Claude Code

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +166 to +171
if (!File.Exists(path) || File.ReadAllText(path) != json)
{
File.WriteAllText(path, json);
TryRestrictPermissions(path);
DebugLogger.Log("Refreshed ~/.claude/.credentials.json from the macOS Keychain");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

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");
      }

Comment on lines +216 to +220
var stdout = process.StandardOutput.ReadToEnd();
process.StandardError.ReadToEnd();
process.WaitForExit();

return process.ExitCode == 0 ? stdout.Trim() : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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}";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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}";

@GordonBeeming GordonBeeming force-pushed the gb/add-claude-provider branch 5 times, most recently from ea093d4 to 50d4ae1 Compare June 29, 2026 07:49
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>
@GordonBeeming GordonBeeming force-pushed the gb/add-claude-provider branch from c4542a8 to 96ba8f8 Compare June 29, 2026 08:26
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