Skip to content

Instantly share code, notes, and snippets.

@bearlike
Created March 12, 2026 10:59
Show Gist options
  • Select an option

  • Save bearlike/79f80ecd0b456d9529ae898990d8b5f2 to your computer and use it in GitHub Desktop.

Select an option

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