Skip to content

Run a Headless Coding Agent with OpenCode

We'll run a headless coding agent within a microVM using OpenCode, an open-source tool to generate code using large language models (LLMs).

OpenCode occasionally supports free trials for popular LLMs. This guide was written whilst Grok's Coder model was available for free - and at no cost.

OpenCode supports various model providers including GitHub CoPilot, OpenAI, Ollama (self-hosted models), and Anthropic.

This example is very minimal and can be tuned and adapted in many ways to suit your workflow.

The general idea is that everything is orchestrated via an initial userdata script:

  • Install opencode CLI.
  • Configure model authentication using a pre-defined config file ~/.local/share/opencode/auth.json from another machine like your workstation.
  • Set the agent working away as a systemd service, with a prompt set in the userdata file.

This example is one-shot, so it's designed to run to completion once, without any further interaction.

Example of a one-shot execution

Example of a one-shot execution for a sample prompt to create a Go HTTP server, and to test it via curl

Example config

Use slicer new to generate a configuration file:

slicer new opencode \
  --userdata-file opencode.sh \
  >  opencode.yaml

And the --ssh-keys or --github flag to add additional ssh keys so you can connect via SSH to review the logs, and/or scp to pull out any generated code.

On a computer where you've pre-installed and authenticated opencode, copy the opencode auth config file ~/.local/share/opencode/auth.json.

On host create a slicer secret for the opencode auth config file.

sudo mkdir .secrets
# Ensure only root can read/write to the secrets folder.
sudo chmod 700 .secrets

sudo -E cp ~/.local/share/opencode/auth.json .secrets/opencode-auth.json

Then, create opencode.sh:

#!/usr/bin/env bash
# Ubuntu only. Requires: curl + arkade preinstalled.
# Installs opencode, sets pre-auth, creates a daemonized systemd unit "opencode.service".

set -euo pipefail

# Install opencode -> /usr/local/bin
arkade get opencode --path /usr/local/bin >/dev/null
chown ubuntu /usr/local/bin/opencode
chmod +x /usr/local/bin/opencode

# Prep dirs & auth for user "ubuntu" (no group assumption)
for d in /home/ubuntu/workdir /home/ubuntu/.local/share/opencode /home/ubuntu/.local/state /home/ubuntu/.cache; do
  mkdir -p "$d"
  chown ubuntu "$d"
done
cp /run/slicer/secrets/opencode-auth.json /home/ubuntu/.local/share/opencode/auth.json
chown ubuntu /home/ubuntu/.local/share/opencode/auth.json
chmod 600 /home/ubuntu/.local/share/opencode/auth.json

# Task payload (edit as needed)
cat >/home/ubuntu/workdir/task.txt <<'EOF'
Create a new Go program with an HTTP server.
Add GET /healthz returning 201 "Created".
Print a git diff at the end.

Use "arkade system install golang" to install Go into the environment. Then test the program with "go run main.go" and curl localhost:8080/healthz - make sure you kill the program after confirming the correct HTTP code was returned.

Set the PATH variable to include /usr/local/go/bin so that the go command is found.
EOF

chown ubuntu /home/ubuntu/workdir/task.txt
chmod 600 /home/ubuntu/workdir/task.txt

# systemd service (daemonized under systemd)
cat >/etc/systemd/system/opencode.service <<'EOF'
[Unit]
Description=OpenCode one-shot (daemonized under systemd)
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/workdir
Environment=HOME=/home/ubuntu
Environment=XDG_STATE_HOME=/home/ubuntu/.local/state
Environment=XDG_CACHE_HOME=/home/ubuntu/.cache
Environment=XDG_DATA_HOME=/home/ubuntu/.local/share
ExecStart=/usr/bin/env bash -lc '/usr/local/bin/opencode run "$(cat /home/ubuntu/workdir/task.txt)" -m opencode/grok-code'
Restart=no
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now opencode.service

Just before you start up the VM, make sure you customise the prompt to have the agent do whatever it is you need.

cat >/home/ubuntu/workdir/task.txt <<'EOF'

Prompt goes here
Over multiple lines

EOF

Start up the VM i.e. sudo -E slicer up ./opencode.yaml

What if you want to copy in a private Git repository?

One option is to include an SSH key for the agent/ubuntu user, so that it can clone the repository directly from GitHub or another Git server. To keep permissions tight, you could also simply scp the code into the microVM after it has booted, like below.

If you want to work on a private Git repository, simply have the systemd unit wait until it finds a folder within the workdir folder, and then scp the code from your host after the microVM has booted.

So if the directory was named i.e. arkade, and we'd cloned it locally, you could amend the opencode systemd unit like so:

[Service]
ExecStartPre=/usr/bin/env bash -c 'while [ ! -d /home/ubuntu/workdir/arkade ]; do sleep 5; done'
ExecStart=/usr/bin/env bash -lc '/usr/local/bin/opencode run "$(cat /home/ubuntu/workdir/task.txt)" -m opencode/grok-code'

View the results

Once the VM is running, you can check the status of the opencode service.

The code will be written to the $HOME/workdir directory.

ssh ubuntu@192.168.137.2

sudo journalctl -u opencode.service -f

cd workdir
find .

git diff

In the example of the preloaded prompt from above, we saw in ~/workdir/main.go:

package main

import (
        "fmt"
        "net/http"
)

func main() {
        http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
                w.WriteHeader(http.StatusOK)
                fmt.Fprint(w, "ok")
        })
        http.ListenAndServe(":8080", nil)
}

To copy out the workdir, run something like this on your host:

mkdir -p agent-runs
cd agent-runs
scp -r ubuntu@192.168.137.2:/home/ubuntu/workdir .

Running an opencode agent via the exec and cp APIs

You can also run an opencode agent from bash after booting up a host, and syncing in a secret for the API keys.

slicer new opencode \
  --userdata-file userdata.sh \
  >  opencode.yaml

Create your userdata to only setup opencode, its workdir, and to sync the API secret to the correct location from the host.

userdata.sh

#!/usr/bin/env bash

set -euo pipefail

# Install opencode -> /usr/local/bin
arkade get opencode --path /usr/local/bin >/dev/null
chown ubuntu /usr/local/bin/opencode
chmod +x /usr/local/bin/opencode

# Prep dirs & auth for user "ubuntu" (no group assumption)
for d in /home/ubuntu/workdir /home/ubuntu/.local/share/opencode /home/ubuntu/.local/state /home/ubuntu/.cache; do
  mkdir -p "$d"
  chown ubuntu "$d"
done
cp /run/slicer/secrets/opencode-auth.json /home/ubuntu/.local/share/opencode/auth.json
chown ubuntu /home/ubuntu/.local/share/opencode/auth.json
chmod 600 /home/ubuntu/.local/share/opencode/auth.json

Authenticate to opencode on your host, then copy the auth file to the .secrets folder relative to where you created opencode.yaml.

#opencode auth login --provider github --token <your-github-token>

mkdir -p .secrets
chmod 700 .secrets
cp ~/.local/share/opencode/auth.json .secrets/opencode-auth.json
chmod 600 .secrets/opencode-auth.json

Now boot up the microVM, just before userdata it will synchronise any secrets placed in the .secrets folder into the microVM.

sudo -E slicer up ./opencode.yaml

The following command will indicate whether the userdata script has completed:

sudo -E slicer vm health opencode-1 
HOSTNAME                  AGENT UPTIME         SYSTEM UPTIME        AGENT VERSION   USERDATA RAN
--------                  ------------         -------------        -------------   ------------
opencode-1                53s                  53s                  0.1.55          1   

If you're automating the process from bash, you can also run --json for something you can parse with jq.

Define a prompt and use the cp command to sync it into the microVM:

cat > task.txt <<'EOF'

Write a simple Go program that prints "Hello, World!" to the console.

EOF
sudo -E slicer vm cp --uid 1000 ./task.txt opencode-1:/home/ubuntu/workdir/task.txt

Next, use the exec command to run the opencode agent interactively, streaming the response back to the host.

sudo -E slicer vm exec --uid 1000 --cwd /home/ubuntu/workdir opencode-1 -- opencode run -m opencode/grok-code $(cat task.txt)

# Example output:

|  Write    home/ubuntu/workdir/hello.go
Created hello.go

You can now see the response in the terminal.

Finally, copy out the results from the microVM via cp in the alternate direction.

sudo -E slicer vm cp --uid 1000 opencode-1:/home/ubuntu/workdir/main.go ./main.go

Examine the output:

package main

import "fmt"

func main() {
        fmt.Println("Hello, World!")
}

It's also possible to copy back the whole workdir folder, which is ideal when opencode has created multiple files and folders.

mkdir -p workdir-out
sudo -E slicer vm cp --mode=tar --uid 1000 opencode-1:/home/ubuntu/workdir/ ./workdir-out

Then, examine the contents:

sudo find workdir-out

workdir-out
workdir-out/main.go
workdir-out/hello.go
workdir-out/workdir
workdir-out/task.txt

Taking opencode automation further

Once a microVM has been started, you can run the opencode agent in prompt mode any number of times, or limit it to just a single prompt.

One advantage of keeping a microVM alive, is that you can follow-up if the results are not as expected, or give it further direction.

A practical application of opencode in a Slicer sandbox, is for code reviews linked to a Source Control Management (SCM) system like GitHub or GitLab.

The CLI can be driven manually or via bash, for our own code review bot, we used the Go SDK.