Skip to content

Commit 9079b52

Browse files
author
Christer Barreholm
committed
feat: switch to opencode, installed from pinned release tarball with sha256 verify
Replace the claude.ai curl|bash installer with a pinned opencode release tarball (v1.18.13) from anomalyco/opencode, verified at build time against sha256 checksums committed in devops/build-image.sh. The binary installs to /usr/local/bin, root-owned, so the container user cannot replace it. Autoupdate is disabled via OPENCODE_DISABLE_AUTOUPDATE=true baked into the image, plus "autoupdate": false in opencode.json (baked in, and written to the host config by the wrapper if none exists). - rename bin/claude to bin/opencode; mount opencode's XDG dirs (~/.config/opencode, ~/.local/share/opencode, ~/.local/state/opencode) - rename container user claude -> opencode; add git for snapshots - add tests for the autoupdate env var, baked config, and root-owned binary - fix CI passing -repo-name instead of --repo-name (was silently ignored); skip registry push on pull requests
1 parent 4bacf4b commit 9079b52

5 files changed

Lines changed: 210 additions & 86 deletions

File tree

.github/workflows/build-and-push.yml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ jobs:
3434
REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
3535
3636
# Run the build script
37-
VERSION=$(./devops/build-image.sh -repo-name $REPO_NAME | tail -n2 | head -n1 | cut -d: -f2)
37+
VERSION=$(./devops/build-image.sh --repo-name $REPO_NAME | tail -n2 | head -n1 | cut -d: -f2)
3838
3939
# Tag the image for GHCR
40-
buildah tag claude-code \
41-
"ghcr.io/$REPO_NAME/claude-code:$VERSION" \
42-
"ghcr.io/$REPO_NAME/claude-code:latest"
40+
buildah tag opencode \
41+
"ghcr.io/$REPO_NAME/opencode:$VERSION" \
42+
"ghcr.io/$REPO_NAME/opencode:latest"
4343
4444
echo "repo_name=$REPO_NAME" >> "$GITHUB_OUTPUT"
4545
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
@@ -48,6 +48,7 @@ jobs:
4848
run: ./tests/test-image.sh "${{ steps.build.outputs.version }}"
4949

5050
- name: Push to GitHub Container Registry
51+
if: github.event_name != 'pull_request'
5152
run: |
52-
buildah push "ghcr.io/${{ steps.build.outputs.repo_name }}/claude-code:${{ steps.build.outputs.version }}"
53-
buildah push "ghcr.io/${{ steps.build.outputs.repo_name }}/claude-code:latest"
53+
buildah push "ghcr.io/${{ steps.build.outputs.repo_name }}/opencode:${{ steps.build.outputs.version }}"
54+
buildah push "ghcr.io/${{ steps.build.outputs.repo_name }}/opencode:latest"

README.md

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,66 @@
1-
claude-podman
1+
opencode-podman
22
====
33

4-
Claude for the security-conscious: run [claude-code, the claude cli tool](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview), in a rootless [podman](https://podman.io/) container.
4+
[opencode](https://opencode.ai/) for the security-conscious: run the opencode
5+
terminal agent in a rootless [podman](https://podman.io/) container.
6+
7+
This is a fork of [claude-podman](https://github.com/EvanCarroll/claude-podman)
8+
adapted for opencode.
59

610
Installation
711
----
812

913
First, download and install podman. Then install the script with curl. Set
10-
`REPO` to match your fork (e.g. `?????/claude-podman`):
14+
`REPO` to match your fork (e.g. `?????/opencode-podman`):
1115

1216
```sh
13-
REPO=evancarroll/claude-podman
17+
REPO=ingby/opencode-podman
1418
curl --proto '=https' --tlsv1.2 -sSf \
15-
"https://raw.githubusercontent.com/$REPO/refs/heads/main/bin/claude" |
19+
"https://raw.githubusercontent.com/$REPO/refs/heads/main/bin/opencode" |
1620
sed "s|^REPO_NAME=.*|REPO_NAME=$REPO|" |
17-
sudo tee /usr/local/bin/claude-podman > /dev/null
18-
sudo chmod a+x /usr/local/bin/claude-podman
21+
sudo tee /usr/local/bin/opencode-podman > /dev/null
22+
sudo chmod a+x /usr/local/bin/opencode-podman
1923
```
2024

21-
Now you can just run `claude-podman`.
25+
Now you can just run `opencode-podman`.
2226

2327
Benefits
2428
----
2529

2630
This provides the following benefits:
2731

28-
* Claude only gets file access to
32+
* opencode only gets file access to
2933
* Files in the present working directory
30-
* `$HOME/.claude.json`
31-
* `$HOME/.claude`
32-
* Claude can only execute the files that exist in the image.
34+
* `$HOME/.config/opencode` (config)
35+
* `$HOME/.local/share/opencode` (auth, sessions)
36+
* `$HOME/.local/state/opencode` (state)
37+
* opencode can only execute the files that exist in the image.
3338

3439
This image runs in rootless podman, and even inside rootless podman it runs as
35-
a non-root user inside the container. Claude code is maximally locked down and
36-
can't even update itself!
40+
a non-root user inside the container.
41+
42+
Supply chain hardening
43+
----
44+
45+
The image does **not** use opencode's `curl | bash` installer, and opencode
46+
cannot update itself:
47+
48+
* The build downloads a single, explicitly pinned release tarball
49+
(`opencode-linux-*-musl.tar.gz`) from GitHub. The version **and** its
50+
sha256 checksum are committed in `devops/build-image.sh`; the build fails
51+
if the downloaded artifact does not match.
52+
* Autoupdate is disabled three ways:
53+
* `OPENCODE_DISABLE_AUTOUPDATE=true` is baked into the image (authoritative
54+
— it wins even if a mounted config says otherwise).
55+
* The image ships `~/.config/opencode/opencode.json` with
56+
`"autoupdate": false`, and the wrapper writes the same config on the
57+
host if none exists yet.
58+
* The binary lives at `/usr/local/bin/opencode`, owned by root, so the
59+
container user could not overwrite it anyway.
60+
61+
To upgrade opencode: edit `OPENCODE_VERSION` and the two `OPENCODE_SHA256_*`
62+
values in `devops/build-image.sh` (download the new tarballs and run
63+
`sha256sum` on them), then rebuild.
3764

3865
Customizing the runtime
3966
----
@@ -46,12 +73,13 @@ Need to add packages to the container, or run an init script? no problem
4673
```
4774

4875

49-
For example, let's say you're using kubernetes and you do want claude to be able to troubleshoot it.
76+
For example, let's say you're using kubernetes and you do want opencode to be
77+
able to troubleshoot it.
5078

5179
```sh
52-
claude-podman \
80+
opencode-podman \
5381
--apk-packages kubectl \
54-
--podman-arg "-v $HOME/.kube/config:/home/claude/.kube/config"
82+
--podman-arg "-v $HOME/.kube/config:/home/opencode/.kube/config"
5583
```
5684

5785
Sharing a network with another container
@@ -64,11 +92,12 @@ container on the same network is then reachable from inside by its container
6492
name, thanks to podman's built-in DNS.
6593

6694
```sh
67-
# Start the service Claude should reach, on a shared network
68-
podman run -d --name myservice --network claude-net some/image
95+
# Start the service opencode should reach, on a shared network
96+
podman run -d --name myservice --network opencode-net some/image
6997

70-
# Run Claude on the same network (creates claude-net if needed)
71-
claude-podman --network claude-net
98+
# Run opencode on the same network (creates opencode-net if needed)
99+
opencode-podman --network opencode-net
72100
```
73101

74-
Inside the container, Claude can now reach the service at `http://myservice:<port>`.
102+
Inside the container, opencode can now reach the service at
103+
`http://myservice:<port>`.

bin/claude renamed to bin/opencode

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/bin/bash
22

33
# GH Repo - Replace to run a fork
4-
REPO_NAME=evancarroll/claude-podman
4+
REPO_NAME=ingby/opencode-podman
55
# Default image
6-
IMAGE="ghcr.io/$REPO_NAME/claude-code:latest"
7-
NAME="claudecode-$(uuidgen -r | cut -d- -f1)"
6+
IMAGE="ghcr.io/$REPO_NAME/opencode:latest"
7+
NAME="opencode-$(uuidgen -r | cut -d- -f1)"
88
INIT_SCRIPT=""
99
APK_PACKAGES=""
1010
PODMAN_ARGS=""
@@ -15,33 +15,33 @@ while [[ $# -gt 0 ]]; do
1515
case "$1" in
1616
--help)
1717
cat <<-'EOT'
18-
Usage: claude [OPTIONS]
18+
Usage: opencode-podman [OPTIONS]
1919
20-
Run Claude Code in a Podman container
20+
Run opencode in a Podman container
2121
2222
Options:
23-
--local Use local image 'claude-code:latest' instead of remote
23+
--local Use local image 'opencode:latest' instead of remote
2424
--init-script FILE Execute initialization script
2525
--apk-packages LIST Install additional Alpine packages (comma or space separated)
2626
--network NAME Join a dedicated podman network (created if missing) so
27-
Claude can reach other containers on it by name via DNS
27+
opencode can reach other containers on it by name via DNS
2828
--podman-arg ARG Pass additional argument to podman
29-
--version Show the Claude Code version in the image
29+
--version Show the opencode version in the image
3030
--help Display this help message
3131
EOT
3232
exit 0
3333
;;
3434
--self-update)
3535
curl --proto '=https' --tlsv1.2 -sSf \
36-
"https://raw.githubusercontent.com/$REPO_NAME/refs/heads/main/bin/claude" |
36+
"https://raw.githubusercontent.com/$REPO_NAME/refs/heads/main/bin/opencode" |
3737
sed "s|^REPO_NAME=.*|REPO_NAME=$REPO_NAME|" |
38-
sudo tee /usr/local/bin/claude-podman >/dev/null
39-
sudo chmod a+x /usr/local/bin/claude-podman
38+
sudo tee /usr/local/bin/opencode-podman >/dev/null
39+
sudo chmod a+x /usr/local/bin/opencode-podman
4040
echo "Updated"
4141
exit 0
4242
;;
4343
--local)
44-
IMAGE="localhost/claude-code:latest"
44+
IMAGE="localhost/opencode:latest"
4545
shift
4646
;;
4747
--init-script)
@@ -63,8 +63,8 @@ while [[ $# -gt 0 ]]; do
6363
--version)
6464
podman run \
6565
--rm \
66-
--user claude \
67-
--userns=keep-id:uid=1001,gid=1001 \
66+
--user opencode \
67+
--userns=keep-id:uid=1000,gid=1000 \
6868
$PODMAN_ARGS \
6969
"$IMAGE" --version
7070
exit 0
@@ -84,16 +84,35 @@ if [ -n "$NETWORK" ]; then
8484
NETWORK_ARG="--network $NETWORK"
8585
fi
8686

87+
# opencode keeps auth/sessions, config, and state in XDG directories;
88+
# create them on the host so podman doesn't have to
89+
mkdir -p \
90+
"${HOME}/.config/opencode" \
91+
"${HOME}/.local/share/opencode" \
92+
"${HOME}/.local/state/opencode"
93+
94+
# Disable autoupdate in the host config unless one already exists (the
95+
# image also sets OPENCODE_DISABLE_AUTOUPDATE, which wins regardless)
96+
if [ ! -f "${HOME}/.config/opencode/opencode.json" ]; then
97+
cat > "${HOME}/.config/opencode/opencode.json" <<-'EOT'
98+
{
99+
"$schema": "https://opencode.ai/config.json",
100+
"autoupdate": false
101+
}
102+
EOT
103+
fi
104+
87105
podman run \
88-
-v "${HOME}/.claude:/home/claude/.claude" \
89-
-v "${HOME}/.claude.json:/home/claude/.claude.json" \
106+
-v "${HOME}/.config/opencode:/home/opencode/.config/opencode" \
107+
-v "${HOME}/.local/share/opencode:/home/opencode/.local/share/opencode" \
108+
-v "${HOME}/.local/state/opencode:/home/opencode/.local/state/opencode" \
90109
-v "${PWD}:${PWD}" \
91110
-w "$PWD" \
92111
--rm \
93112
--name "$NAME" \
94113
-ti \
95114
--detach \
96-
--user claude \
115+
--user opencode \
97116
--userns=keep-id:uid=1000,gid=1000 \
98117
$NETWORK_ARG \
99118
$PODMAN_ARGS \
@@ -108,4 +127,4 @@ if [ -n "$INIT_SCRIPT" ]; then
108127
podman exec --user root:root "$NAME" bash /root/init.sh
109128
fi
110129

111-
podman container attach "$NAME"
130+
podman container attach "$NAME"

devops/build-image.sh

Lines changed: 69 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
#!/bin/sh
2+
set -eu
23

3-
CONTAINER=$(buildah from docker.io/alpine:latest)
4-
IMAGE=claude-code
5-
REPO_NAME="EvanCarroll/claude-podman"
4+
# opencode release pin. To upgrade: bump the version, then update both
5+
# checksums from the new release assets (compute with sha256sum after
6+
# downloading from https://github.com/anomalyco/opencode/releases).
7+
OPENCODE_VERSION="1.18.13"
8+
OPENCODE_SHA256_X86_64="93f1506f26ad8b6e867754d144bea43e6c707ec518bac227fdf959048d020d74"
9+
OPENCODE_SHA256_AARCH64="a5b90d6111d2d826fe14952c769d0d4bcf00db7810c0c92d9f4541d3768f03ef"
10+
11+
IMAGE=opencode
12+
REPO_NAME="ingby/opencode-podman"
613

714
# Process arguments
815
while [ "$#" -gt 0 ]; do
@@ -11,7 +18,7 @@ while [ "$#" -gt 0 ]; do
1118
cat <<-'EOT'
1219
Usage: $0 [OPTIONS]
1320
14-
Build image to run Claude Code in a Podman container
21+
Build image to run opencode in a Podman container
1522
1623
Options:
1724
--repo-name REPO_NAME Use the specified repo name instead of the default "REPO_NAME"
@@ -29,30 +36,70 @@ while [ "$#" -gt 0 ]; do
2936
esac
3037
done
3138

39+
case "$(uname -m)" in
40+
x86_64)
41+
OPENCODE_ASSET="opencode-linux-x64-musl.tar.gz"
42+
OPENCODE_SHA256="$OPENCODE_SHA256_X86_64"
43+
;;
44+
aarch64 | arm64)
45+
OPENCODE_ASSET="opencode-linux-arm64-musl.tar.gz"
46+
OPENCODE_SHA256="$OPENCODE_SHA256_AARCH64"
47+
;;
48+
*)
49+
echo "Unsupported architecture: $(uname -m)" >&2
50+
exit 1
51+
;;
52+
esac
53+
54+
CONTAINER=$(buildah from docker.io/alpine:latest)
55+
3256
# Install dependencies and create user
3357
buildah run "$CONTAINER" sh <<EOT
34-
apk add --no-cache bash curl libgcc libstdc++ ripgrep
35-
adduser -D claude
58+
set -eu
59+
apk add --no-cache bash curl git libgcc libstdc++ ripgrep
60+
adduser -D opencode
3661
EOT
3762

38-
# Install Claude Code using native installer as the claude user
39-
buildah run --user claude "$CONTAINER" bash -c 'curl -fsSL https://claude.ai/install.sh | bash'
63+
# Install opencode from a pinned GitHub release tarball, verifying its
64+
# sha256 against the checksum committed in this repo. The binary is
65+
# installed root-owned outside the user's home so opencode cannot
66+
# replace itself even if an update were attempted.
67+
buildah run "$CONTAINER" sh <<EOT
68+
set -eu
69+
cd /tmp
70+
curl --proto '=https' --tlsv1.2 -fsSLo "$OPENCODE_ASSET" \
71+
"https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/${OPENCODE_ASSET}"
72+
echo "$OPENCODE_SHA256 $OPENCODE_ASSET" | sha256sum -c -
73+
tar -xzf "$OPENCODE_ASSET" -C /usr/local/bin opencode
74+
chown root:root /usr/local/bin/opencode
75+
chmod 755 /usr/local/bin/opencode
76+
rm -f "$OPENCODE_ASSET"
77+
EOT
4078

41-
# Get the installed version
42-
CLAUDE_VERSION=$(buildah run --user claude "$CONTAINER" /home/claude/.local/bin/claude --version 2>/dev/null | head -1 | awk '{print $1}')
79+
# Bake a default config that disables autoupdate. If the config dir is
80+
# bind-mounted at runtime this file is shadowed, so the
81+
# OPENCODE_DISABLE_AUTOUPDATE env var below is the authoritative switch.
82+
buildah run --user opencode "$CONTAINER" sh <<'EOT'
83+
set -eu
84+
mkdir -p /home/opencode/.config/opencode
85+
cat > /home/opencode/.config/opencode/opencode.json <<'JSON'
86+
{
87+
"$schema": "https://opencode.ai/config.json",
88+
"autoupdate": false
89+
}
90+
JSON
91+
EOT
4392

4493
buildah config \
45-
--author "Evan Carroll" \
46-
--env "PATH=/home/claude/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
94+
--author "Christer Barreholm" \
95+
--env "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
4796
--env "SHELL=/bin/bash" \
48-
--env "DISABLE_TELEMETRY=1" \
49-
--env "DISABLE_AUTOUPDATER=1" \
50-
--env "USE_BUILTIN_RIPGREP=0" \
97+
--env "OPENCODE_DISABLE_AUTOUPDATE=true" \
5198
--cmd "" \
52-
--entrypoint '[ "/home/claude/.local/bin/claude" ]' \
53-
--annotation "org.anthropic.claudecode.version=$CLAUDE_VERSION" \
54-
--annotation "org.opencontainers.image.title=claude-code" \
55-
--annotation "org.opencontainers.image.description=Claude Code on Alpine ready for rootless podman" \
99+
--entrypoint '[ "/usr/local/bin/opencode" ]' \
100+
--annotation "ai.opencode.version=$OPENCODE_VERSION" \
101+
--annotation "org.opencontainers.image.title=opencode" \
102+
--annotation "org.opencontainers.image.description=opencode on Alpine ready for rootless podman" \
56103
--annotation "org.opencontainers.image.url=https://github.com/$REPO_NAME" \
57104
--annotation "org.opencontainers.image.source=https://github.com/$REPO_NAME" \
58105
--annotation "org.opencontainers.image.documentation=https://github.com/$REPO_NAME/blob/main/README.md" \
@@ -64,8 +111,8 @@ buildah commit \
64111
--rm \
65112
"$CONTAINER" "$IMAGE"
66113

67-
buildah tag "$IMAGE" "$IMAGE:$CLAUDE_VERSION"
114+
buildah tag "$IMAGE" "$IMAGE:$OPENCODE_VERSION"
68115

69116
echo Done!
70-
echo ${IMAGE}:${CLAUDE_VERSION}
71-
echo To use this image run /bin/claude
117+
echo ${IMAGE}:${OPENCODE_VERSION}
118+
echo To use this image run /bin/opencode

0 commit comments

Comments
 (0)