- Python 96%
- Shell 4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .gitignore | ||
| agent.py | ||
| claude-agent.service | ||
| CLAUDE.md | ||
| grant-access.sh | ||
| mcp.json | ||
| README.md | ||
| webhook.py | ||
claude-agent
Claude Code working Forgejo issues in any of the owner's repositories. Assign
an issue to the claude user, or mention @claude in a new issue or a comment, and a headless
Claude run picks it up, commits to claude/issue-<n>, opens a PR and reports
back on the thread. Follow-ups go as @claude ... comments on the issue or on
the PR; each one is a fresh Claude run that gets the whole thread plus the
existing branch.
Quick start
WEBHOOK_SECRET=secret LISTEN=127.0.0.1:8099 python3 webhook.py # curl localhost:8099/healthz
Python 3 standard library only. Real runs also need CLAUDE_BOT_TOKEN, CLAUDE_CODE_OAUTH_TOKEN and
FORGEJO_URL (see below). Developer and agent notes, gotchas: CLAUDE.md.
Pieces
| What | Where |
|---|---|
| Webhook receiver | webhook.py, claude-agent.service |
| Harness (one run) | agent.py |
| Host | VM 106 claude-agent on Proxmox, 192.168.88.106, user claude, checkout in ~/claude-agent |
| Trigger | user-level webhook on ivan.pavlina → http://192.168.88.106:8090/, events: Issues, Issue Assign, Issue Comment, Pull Request Comment |
| Bot account | Forgejo user claude, collaborator (write) on each repo |
Forgejo only delivers to private addresses listed in
FORGEJO__webhook__ALLOWED_HOST_LIST (docksys compose).
A new repository
./grant-access.sh ivan.pavlina/<repo> # no argument = all non-archived repos
The no-argument run skips claude-agent itself: the bot must not propose
changes to its own harness.
Without access the bot answers on the issue and asks for it.
Deploying a change
ssh claude@192.168.88.106 'cd ~/claude-agent && git pull && sudo systemctl restart claude-agent'
There is no CI for this repo; deploying is the pull and restart above.
Run logs: ~/runs/<timestamp>-<owner>_<repo>-<n>.log (payload next to it, kept
30 days), service log: journalctl -u claude-agent.
The VM
- Debian 13 cloud image, 4 vCPU (affinity 12-19, E-cores), 8 GB RAM.
scsi040G: OS,~/.claude(settings, skills, memory, sessions), this checkout,~/runs, secrets. Included in the nightly vzdump topbs-local.scsi1100G,backup=0, mounted at/cache: run checkouts (TMPDIR=/cache/runner), Docker data-root (/cache/docker), and~/.cache,~/.npm,~/go(symlinks into/cache/home). Losing it costs nothing but rebuild time.- Secrets in
~/.config/claude-agent/agent.env(0600):CLAUDE_BOT_TOKEN(bot user),CLAUDE_CODE_OAUTH_TOKEN(fromclaude setup-token, subscription auth),WEBHOOK_SECRET,FORGEJO_URL.
Containment
- Only logins in
ALLOWED_USERS(defaultivan.pavlina) can trigger a run, and every delivery must carry a valid HMAC signature. - The bot token is stripped from Claude's environment and kept out of the
clone's remote URL, so Claude does not push; the harness pushes, only to the
work branch, never to the default branch. This is not a hard boundary: Claude
runs as the same user and could read
agent.envfrom disk. Acceptable here because only the owner can trigger runs and the bot can only reach branches and PRs of repos it was added to; the real fix would be a separate Unix user. - Tools: file reads/edits plus the Bash allowlist
ALLOWED_TOOLSinagent.py. Anything else is denied and listed in the run log. A repo can allow more in its own.claude/settings.json. - Browser: where
/usr/bin/chromiumexists, the run gets the Playwright MCP server frommcp.json(headless, isolated profile) to try web GUIs on desktop and phone sizes. - Permission mode:
CLAUDE_PERMISSION_MODE(defaultauto); effort:CLAUDE_EFFORT(unset = CLI default). Both go inagent.env.