Sandboxed AI agents (Claude Code or Codex) open PRs against a local Gitea; you review in the Gitea UI, merge, then sync to GitHub. The sandbox gets no host credentials — only Gitea's port, with a per-agent token baked in.
sbx agent (claude|codex) → gitea (local :3000) → you review → merge → sync → GitHub (origin)
Each tool acts as its own Gitea user (claude, codex), so PRs stay attributable. Both run
the same agent-side gitea-* scripts; only instruction delivery differs (a Claude skill vs.
an inlined ~/.codex/AGENTS.md, since Codex has no skills).
Docker + sbx (Docker Sandboxes), plus git, curl, jq on the host.
Everything is reachable through one wf command; source env.sh puts it on PATH (with tab
completion) and keeps the raw operator/ scripts available too.
docker compose up -d # Gitea at :3000 (data persists in volume gitea_data)
# complete the first-run wizard, create your user
source env.sh
wf setup <user> <pass> # your token + agent users 'claude' & 'codex' → ./.gitea-workflow
wf policy allow # open Gitea's egress port for sandboxes
wf build # bake the Claude template; add --codex for the Codex oneTo bake extra plugins/skills/tools into the template, edit bootstrap.sh (it runs in the
builder with WORKFLOW_AGENT_KIND set) and allow its install domains via wf policy first.
wf register [repo] # once per repo: create in Gitea, add `gitea` remote, push
cd /path/to/repo && wf new [--codex] # spawn + attach an agent sandbox (add a name for a 2nd)Inside the sandbox the agent opens and iterates on the PR (driven by the open-pr skill):
gitea-pr <base> "<title>" "<body>" # push branch + open PR
gitea-push # push follow-ups after you review; the PR updatesReview in the Gitea UI. After merging, sync back to GitHub from the host:
wf sync <branch> # fast-forward the merged branch → originWorth knowing:
- The PR's base branch must already exist in Gitea, or PR creation returns 404.
wf newmakes a read-only--clonesandbox (host tree untouched); run it again for parallel agents.GITEA_SBX_CLONE=0bind-mounts instead.wf registergrants both agent users write and installs a squash merge-message template so merges yield clean commits. It prompts to push the current branch or the whole repo (GITEA_PUSH_SCOPE=branch|allto skip); LFS blobs are never uploaded.
wf help [command] for details. Subcommands forward to operator/ scripts / Makefile targets:
| Command | Does |
|---|---|
wf setup <user> <pass> |
Create your token + agent users/tokens; write ./.gitea-workflow. |
wf build [--codex] |
(Re)build a template gitea-workflow-<kind>:v1. |
wf policy allow|deny|show |
Manage the Gitea egress rule. |
wf register [repo] |
Register a repo in Gitea (remote, push, merge template, agent write). |
wf new [--codex] [name] [repo] |
Spawn + attach a sandbox agent. |
wf sync [branch] [repo] |
Fast-forward a merged branch → origin. |
wf prune |
Remove stopped sbx sandboxes. |
wf wipe |
Tear down the installation. |
Agent-side commands (baked in, on PATH in the sandbox): gitea-pr, gitea-push,
gitea-api <method> <path> [json] (authed API — read/post review comments), gitea-pr-watch
(poll for reviewer activity), gitea-version.
Three layers keep the sandbox isolated:
- Egress —
sbxdefault-denies;wf policy allowopens Gitea's port only. - Config + instructions — baked into the template:
~/.gitea-workflow, thegitea-*scripts, and PR instructions in the tool's native format (Claudeopen-prskill +CLAUDE.md, or an inlined~/.codex/AGENTS.md). - Wire auth — token sent as an
Authorization: tokenheader, never in the URL.
Two gotchas baked around by build-template.sh:
localhostvshost.docker.internal. Agents reachhost.docker.internal:3000, but thesbxproxy matches policy againstlocalhost, so the egress rule islocalhost:3000whileGITEA_URLstayshost.docker.internal:3000.- Two config files. Host
./.gitea-workflowtargetslocalhost:3000; the baked copy targetshost.docker.internal:3000. Same token, different reachable URL.
Security. Each agent token lives in its template's image layers and in ./.gitea-workflow
(gitignored). Keep both local — never sbx template save --output to share, and rebuild after
rotating a token.
| Symptom | Fix |
|---|---|
Blocked by network policy: domain localhost:3000 |
wf policy allow (rule is localhost:3000). |
401 invalid token from a sandbox |
Source ~/.gitea-workflow, or rebuild the template. |
Push works but PR returns 404 |
Base branch doesn't exist in Gitea — use an existing base. |
curl prints 000 |
No connection — $GITEA_URL is empty (config not sourced). |
| Script changes not reflected in the sandbox | The template is a snapshot — wf build again. |
wf sync "fast-forward failed" |
Branch diverged — inspect git log HEAD..gitea/<branch> --oneline. |
| Lost the API token | Re-run wf setup (delete the old one at /user/settings/applications). |