Created
March 12, 2026 10:59
-
-
Save bearlike/79f80ecd0b456d9529ae898990d8b5f2 to your computer and use it in GitHub Desktop.
GitHub Action CI for running Claude Code proxied via WireGuard. It uses a custom endpoint and model identifier.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Claude Code proxied via WireGuard. Uses a custom endpoint and model identifier. | |
| # Add a .claude/.mcp.json file to the workspace root to allow the Claude Code action to use the Tools MCP server. | |
| name: Claude LiteLLM WireGuard Runtime | |
| description: Shared Claude Code runtime for GitHub Actions using WireGuard and a private LiteLLM endpoint. | |
| inputs: | |
| working_directory: | |
| description: Repository-relative directory Claude should operate in. | |
| required: true | |
| prompt: | |
| description: Prompt to send to Claude Code. | |
| required: true | |
| allowed_paths: | |
| description: Newline-delimited list of repository-relative paths Claude is allowed to edit. | |
| required: true | |
| default_litellm_base_url: | |
| description: Default LiteLLM base URL for this workflow. | |
| required: true | |
| default_claude_model: | |
| description: Default model identifier for this workflow. | |
| required: true | |
| litellm_base_url: | |
| description: Optional override for the LiteLLM base URL. | |
| required: false | |
| default: "" | |
| claude_model: | |
| description: Optional override for the Claude model. | |
| required: false | |
| default: "" | |
| max_turns: | |
| description: Maximum Claude turns for this invocation. | |
| required: false | |
| default: "45" | |
| timeout_minutes: | |
| description: Claude runtime timeout in minutes. | |
| required: false | |
| default: "15" | |
| api_timeout_ms: | |
| description: API timeout passed through to Claude. | |
| required: false | |
| default: "840000" | |
| keepalive: | |
| description: WireGuard persistent keepalive interval. | |
| required: false | |
| default: "5" | |
| wg_endpoint: | |
| description: WireGuard endpoint in host:port format. | |
| required: true | |
| wg_endpoint_public_key: | |
| description: WireGuard server public key. | |
| required: true | |
| wg_client_private_key: | |
| description: WireGuard client private key. | |
| required: true | |
| wg_preshared_key: | |
| description: Optional WireGuard preshared key. | |
| required: false | |
| default: "" | |
| wg_client_ips: | |
| description: WireGuard client IP assignments. | |
| required: true | |
| wg_allowed_ips: | |
| description: WireGuard allowed IP ranges. | |
| required: true | |
| litellm_api_key: | |
| description: API key for LiteLLM / Anthropic-compatible auth. | |
| required: true | |
| tools_mcp_bearer_token: | |
| description: Bearer token for the Tools MCP server. | |
| required: true | |
| outputs: | |
| anthropic_base_url: | |
| description: Normalized Anthropic-compatible base URL used for the run. | |
| value: ${{ steps.config.outputs.anthropic_base_url }} | |
| claude_model: | |
| description: Model identifier used for the run. | |
| value: ${{ steps.config.outputs.claude_model }} | |
| current_datetime: | |
| description: Pacific time captured for the run. | |
| value: ${{ steps.runtime_clock.outputs.current_datetime }} | |
| execution_file: | |
| description: Claude execution file emitted by the base action. | |
| value: ${{ steps.claude.outputs.execution_file }} | |
| claude_result: | |
| description: Parsed concluding Claude result text. | |
| value: ${{ steps.publish_claude.outputs.claude_result }} | |
| runs: | |
| using: composite | |
| steps: | |
| - name: Resolve runtime configuration | |
| id: config | |
| shell: bash | |
| run: | | |
| litellm_base_url="${{ inputs.litellm_base_url }}" | |
| if [[ -z "$litellm_base_url" ]]; then | |
| litellm_base_url="${{ inputs.default_litellm_base_url }}" | |
| fi | |
| claude_model="${{ inputs.claude_model }}" | |
| if [[ -z "$claude_model" ]]; then | |
| claude_model="${{ inputs.default_claude_model }}" | |
| fi | |
| if [[ -z "$litellm_base_url" ]]; then | |
| echo "::error::Missing LiteLLM base URL." | |
| exit 1 | |
| fi | |
| if [[ -z "$claude_model" ]]; then | |
| echo "::error::Missing Claude model." | |
| exit 1 | |
| fi | |
| missing=0 | |
| for required_name in \ | |
| WG_ENDPOINT \ | |
| WG_ENDPOINT_PUBLIC_KEY \ | |
| WG_CLIENT_PRIVATE_KEY \ | |
| WG_CLIENT_IPS \ | |
| WG_ALLOWED_IPS \ | |
| LITELLM_API_KEY \ | |
| tools_mcp_bearer_token | |
| do | |
| if [[ -z "${!required_name:-}" ]]; then | |
| echo "::error::Missing required runtime input: ${required_name}" | |
| missing=1 | |
| fi | |
| done | |
| if [[ "$missing" -ne 0 ]]; then | |
| exit 1 | |
| fi | |
| normalized_base_url="${litellm_base_url%/}" | |
| normalized_base_url="${normalized_base_url%/v1}" | |
| models_url="${normalized_base_url}/v1/models" | |
| echo "anthropic_base_url=${normalized_base_url}" >> "$GITHUB_OUTPUT" | |
| echo "models_url=${models_url}" >> "$GITHUB_OUTPUT" | |
| echo "claude_model=${claude_model}" >> "$GITHUB_OUTPUT" | |
| env: | |
| WG_ENDPOINT: ${{ inputs.wg_endpoint }} | |
| WG_ENDPOINT_PUBLIC_KEY: ${{ inputs.wg_endpoint_public_key }} | |
| WG_CLIENT_PRIVATE_KEY: ${{ inputs.wg_client_private_key }} | |
| WG_CLIENT_IPS: ${{ inputs.wg_client_ips }} | |
| WG_ALLOWED_IPS: ${{ inputs.wg_allowed_ips }} | |
| LITELLM_API_KEY: ${{ inputs.litellm_api_key }} | |
| TOOLS_MCP_BEARER_TOKEN: ${{ inputs.tools_mcp_bearer_token }} | |
| - name: Verify working directory | |
| shell: bash | |
| working-directory: ${{ inputs.working_directory }} | |
| run: | | |
| test "$PWD" = "${GITHUB_WORKSPACE}/${{ inputs.working_directory }}" | |
| - name: Connect to WireGuard | |
| uses: egor-tensin/setup-wireguard@v1 | |
| with: | |
| endpoint: ${{ inputs.wg_endpoint }} | |
| endpoint_public_key: ${{ inputs.wg_endpoint_public_key }} | |
| private_key: ${{ inputs.wg_client_private_key }} | |
| preshared_key: ${{ inputs.wg_preshared_key }} | |
| ips: ${{ inputs.wg_client_ips }} | |
| allowed_ips: ${{ inputs.wg_allowed_ips }} | |
| keepalive: ${{ inputs.keepalive }} | |
| - name: Verify LiteLLM is reachable over VPN | |
| shell: bash | |
| run: | | |
| curl --fail --silent --show-error --max-time 15 \ | |
| -H "Authorization: Bearer ${{ inputs.litellm_api_key }}" \ | |
| "${{ steps.config.outputs.models_url }}" > /dev/null | |
| - name: Capture current Pacific time | |
| id: runtime_clock | |
| shell: bash | |
| run: | | |
| echo "current_datetime=$(TZ=America/Los_Angeles date '+%Y-%m-%d %H:%M:%S %Z')" >> "$GITHUB_OUTPUT" | |
| - name: Run Claude Code | |
| id: claude | |
| uses: anthropics/claude-code-base-action@beta | |
| env: | |
| CLAUDE_WORKING_DIR: ${{ inputs.working_directory }} | |
| ANTHROPIC_BASE_URL: ${{ steps.config.outputs.anthropic_base_url }} | |
| ANTHROPIC_AUTH_TOKEN: ${{ inputs.litellm_api_key }} | |
| ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ steps.config.outputs.claude_model }} | |
| ANTHROPIC_DEFAULT_SONNET_MODEL: ${{ steps.config.outputs.claude_model }} | |
| ANTHROPIC_DEFAULT_HAIKU_MODEL: ${{ steps.config.outputs.claude_model }} | |
| TOOLS_MCP_BEARER_TOKEN: ${{ inputs.tools_mcp_bearer_token }} | |
| CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1" | |
| API_TIMEOUT_MS: ${{ inputs.api_timeout_ms }} | |
| with: | |
| anthropic_api_key: ${{ inputs.litellm_api_key }} | |
| model: ${{ steps.config.outputs.claude_model }} | |
| timeout_minutes: ${{ inputs.timeout_minutes }} | |
| prompt: ${{ inputs.prompt }} | |
| settings: | | |
| { | |
| "permissions": { | |
| "defaultMode": "bypassPermissions", | |
| "deny": ["WebFetch", "WebSearch"] | |
| } | |
| } | |
| append_system_prompt: | | |
| Stay right here in the `${{ inputs.working_directory }}` project directory. Read `AGENTS.md` and treat it as your absolute source of truth. | |
| This is a headless, one-shot CI run. Keep scans safe. Finish the refresh in one go and stop right away. | |
| Don't wait for follow-up input. Don't create new files. Never modify non-markdown files. | |
| The only allowed edit targets are: | |
| ${{ inputs.allowed_paths }} | |
| **You must strictly follow any provided output guidelines in the skill.** | |
| Current date and time: ${{ steps.runtime_clock.outputs.current_datetime }} | |
| max_turns: ${{ inputs.max_turns }} | |
| - name: Publish Claude result | |
| id: publish_claude | |
| shell: bash | |
| env: | |
| EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }} | |
| run: | | |
| if [[ -z "$EXECUTION_FILE" || ! -f "$EXECUTION_FILE" ]]; then | |
| echo "::error::Claude execution_file output is missing." | |
| exit 1 | |
| fi | |
| result="$(jq -rs ' | |
| [ | |
| .[] | |
| | if type == "array" then .[] else . end | |
| | select(type == "object") | |
| | if .type == "result" and (.result // "") != "" and .result != "(no content)" then | |
| .result | |
| elif .type == "assistant" then | |
| (.message.content // []) | |
| | map(select(.type == "text") | .text) | |
| | join("\n") | |
| else | |
| empty | |
| end | |
| ] | |
| | map(select(. != "" and . != "(no content)")) | |
| | last // "(no content)" | |
| ' "$EXECUTION_FILE")" | |
| { | |
| echo "## Claude Result" | |
| echo | |
| printf '%s\n' "$result" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| { | |
| echo "claude_result<<EOF" | |
| printf '%s\n' "$result" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment