Cordium documentation · Latest

Claude Code

This example runs Claude Code unattended inside ephemeral Cordium Workspaces to fix bugs and implement small features in a Go repository. Each run clones the repository, lets Claude Code make and test the change, pushes a branch and opens a draft pull request, and then stops and discards the Workspace.

What makes this setup different from running an agent in a typical container or CI runner is that the Workspace holds no credential at all: not the Anthropic API key, not a GitHub token, nothing. Claude Code talks to the Anthropic API, git pushes the branch and curl opens the pull request through Octelium Services that inject the credentials on a per-request basis and only for the Workspaces of a specific Template and repository (read more about secretless access here). A prompt-injected or misbehaving agent therefore has nothing to exfiltrate, and every request it makes is authorized and logged by Octelium.

The Octelium Services

First, a Cluster administrator stores the credentials as Octelium Secrets. The GitHub token is a fine-grained personal access token or a GitHub App installation token that has the Contents and Pull requests read and write permissions on the target repository only:

octeliumctl create secret anthropic-api-key octeliumctl create secret github-agent-token

Then, the administrator creates the following Services:

  • anthropic: an LLM Service that exposes the Anthropic API (read more about LLM Services here).

  • github.payments: an HTTP Service in front of https://github.com that injects the token as HTTP basic authentication, which is what git uses over HTTPS.

  • github-api.payments: an HTTP Service in front of the GitHub REST API that injects the token as a bearer token.

Both GitHub Services are restricted to the Workspaces of the claude-agent Template of the payments Space, and to the acme-corp/payments-api repository:

kind: Service metadata: name: anthropic spec: mode: LLM config: upstream: url: https://api.anthropic.com llm: protocol: ANTHROPIC auth: custom: header: x-api-key value: fromSecret: anthropic-api-key --- kind: Service metadata: name: github.payments spec: mode: HTTP config: upstream: url: https://github.com http: auth: basic: username: x-access-token password: fromSecret: github-agent-token authorization: inlinePolicies: - spec: rules: - effect: ALLOW condition: all: of: - match: '"ext" in ctx.session.status && "cordium" in ctx.session.status.ext' - match: ctx.session.status.ext.cordium.templateRef.name == "claude-agent.payments.cordium" - match: ctx.request.http.path.startsWith("/acme-corp/payments-api.git/") --- kind: Service metadata: name: github-api.payments spec: mode: HTTP config: upstream: url: https://api.github.com http: auth: bearer: fromSecret: github-agent-token authorization: inlinePolicies: - spec: rules: - effect: ALLOW condition: all: of: - match: '"ext" in ctx.session.status && "cordium" in ctx.session.status.ext' - match: ctx.session.status.ext.cordium.templateRef.name == "claude-agent.payments.cordium" - match: ctx.request.http.path.startsWith("/repos/acme-corp/payments-api/")

You can apply them via octeliumctl apply (read more here). The Users who run the agent must also be allowed to access the anthropic Service by a Policy (read more about scoping LLM access here).

note

The inline Policies above make the GitHub Services usable only from the agent's Workspaces and only for one repository. Even the Users who own those Workspaces cannot use the token from their laptops, and the agent cannot push to any other repository or call any other GitHub API.

The Template

Next, a Space admin creates the following Template in the payments Space. Its ON_CREATE tasks install Claude Code and a few tools, and its agent task runs the whole workflow once the Workspace is connected to the Cluster:

spec: image: registry: url: golang:1.26-bookworm vars: - name: REPO value: acme-corp/payments-api - name: BASE value: main - name: MODEL value: claude-opus-5-5 - name: TASK value: "" runtime: autoStop: true envVars: - key: ANTHROPIC_BASE_URL value: http://anthropic # A placeholder. Octelium injects the real API key and never forwards this one. - key: ANTHROPIC_API_KEY value: injected-by-octelium tasks: - name: install-packages type: ON_CREATE runAsRoot: true onFailure: ON_FAILURE_ABORT run: | apt-get update apt-get install -y --no-install-recommends jq ripgrep - name: install-claude-code type: ON_CREATE workingDir: /workspace onFailure: ON_FAILURE_ABORT run: curl -fsSL https://claude.ai/install.sh | bash - name: agent type: POST_START workingDir: /workspace onFailure: ON_FAILURE_ABORT envVars: - key: REPO value: ${{ vars.REPO }} - key: BASE value: ${{ vars.BASE }} - key: MODEL value: ${{ vars.MODEL }} - key: TASK value: ${{ vars.TASK }} run: | set -eu [ -n "$TASK" ] || exit 0 export PATH="$HOME/.local/bin:$PATH" timeout 120 sh -c 'until curl -s -o /dev/null http://github.payments/; do sleep 2; done' git clone --branch "$BASE" "http://github.payments/$REPO.git" repo cd repo BRANCH="agent/$CORDIUM_NAME" git switch -c "$BRANCH" cat > /tmp/prompt.md <<EOF $TASK Work only inside this repository. Run "go vet ./..." and the relevant "go test" packages, and make sure they pass before you finish. Do not commit or push. End with a concise pull request description in Markdown. EOF claude -p "$(cat /tmp/prompt.md)" \ --model "$MODEL" \ --dangerously-skip-permissions \ --output-format text < /dev/null > /tmp/summary.md if [ -z "$(git status --porcelain)" ]; then echo "Claude Code made no changes" exit 0 fi TITLE="$(printf '%s\n' "$TASK" | head -n 1 | cut -c1-72)" git add -A git -c user.name="Claude Code" -c user.email="agents@acme-corp.example" \ commit -q -m "$TITLE" git push -q origin "$BRANCH" jq -n --arg title "$TITLE" --arg head "$BRANCH" --arg base "$BASE" \ --rawfile body /tmp/summary.md \ '{title: $title, head: $head, base: $base, body: $body, draft: true}' | curl -sSf -X POST "http://github-api.payments/repos/$REPO/pulls" \ -H "Accept: application/vnd.github+json" --data @- | jq -r '"Opened " + .html_url' limit: cpu: millicores: 4000 memory: megabytes: 8192 storage: megabytes: 20000

Here are a few notes about this Template:

  • The agent task is a POST_START task since it needs the Workspace's connection to the Cluster, which is established at the beginning of the POST_START phase. It waits for github.payments to become reachable before cloning (read more here).

  • The task variables are passed to the script as environment variables rather than substituted into the script itself, which keeps an arbitrary TASK text from being interpreted by the shell.

  • Claude Code's standard input is redirected from /dev/null, since claude -p otherwise waits for piped input when its standard input is not a terminal.

  • --dangerously-skip-permissions lets Claude Code run commands and edit files without prompting. This is what the Workspace is for: an isolated, disposable sandbox in which the agent runs as an unprivileged user and holds no credentials. You can further restrict its network egress (read more here).

  • autoStop stops the Workspace as soon as the agent task completes, and the ON_FAILURE_ABORT policy makes the run fail if any step fails. All the foreground tasks of a run must complete within 60 minutes (read more here).

Create the Template and pre-build it so that every run starts with the tools already installed (read more about pre-builds here):

cordium create template claude-agent.payments.cordium --file claude-agent.yaml cordium build claude-agent.payments.cordium
note

Pre-builds run the foreground POST_START tasks too, but without any connection to the Cluster. This is why the agent task exits immediately when TASK is empty, which is its default value.

Running the Agent

Every run is a new ephemeral Workspace of the Template with its own TASK:

cordium create ws --template claude-agent.payments.cordium --ephemeral --start \ --var TASK="Fix #1842: TestWebhookRetry is flaky because the retry backoff reads the wall clock. Use the injected clock instead."

You can follow the run live from the web portal or via cordium logs:

cordium logs <WORKSPACE>
Cloning into 'repo'... Switched to a new branch 'agent/q8zr2' Opened https://github.com/acme-corp/payments-api/pull/1851

You can also trigger runs programmatically, for example from an issue tracker webhook or a scheduler, via the SDKs. Here is an example via the Python SDK that starts a run and waits until it stops:

from cordium import Cordium, WorkspaceFailureError with Cordium() as client: workspace = client.workspaces.create( template="claude-agent.payments.cordium", ephemeral=True, vars={"TASK": "Fix #1842: TestWebhookRetry is flaky. Use the injected clock."}, ) workspace.start() try: workspace.wait_until_stopped(timeout=3600) print(f"Workspace {workspace.name} completed") except WorkspaceFailureError as error: print(f"Workspace {workspace.name} failed: {error}") finally: workspace.delete()

Interactive Sessions

The same Template also works for interactive sessions. Create a persistent Workspace without a TASK, open a terminal from the web portal, or via cordium ssh or VS Code, and run claude as usual. Since ANTHROPIC_BASE_URL points to the anthropic Service, Claude Code uses the Cluster's Anthropic API key without any login:

cordium create ws --template claude-agent.payments.cordium --start cordium ssh <WORKSPACE>
cd /workspace && git clone http://github.payments/acme-corp/payments-api.git && cd payments-api claude
note

If your team uses Claude subscriptions instead of API keys, store a token generated via claude setup-token as a UserSecret and inject it via your UserConfig as the CLAUDE_CODE_OAUTH_TOKEN environment variable (read more here). In that case, remove ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY from the Template. Note, however, that the token is then readable inside the Workspace.