Files
claude-hook/README.md
T
2026-09-28 15:09:16 +00:00

109 lines
4.8 KiB
Markdown

# claude-hook
Listens for Gitea webhooks and runs Claude Code against an issue or pull
request whenever the configured owner mentions `@claude` in a comment or
issue body. On success it commits any changes Claude made, pushes a branch,
opens (or reuses) a pull request, and replies on the issue with a summary.
Failures are reported the same way, as a comment, so nothing is silent.
## Layout
```
src/
config.js env parsing + validation, fails fast on startup
logger.js structured JSON logging to stdout/stderr
exec.js process spawning with timeout + output capture
gitea.js Gitea API client (issues, pulls, comments)
git.js git clone/checkout/commit/push for one job's working dir
claude.js invokes the `claude` CLI non-interactively
prompt.js builds the prompt sent to Claude
events.js pure webhook-payload -> trigger classification logic
jobQueue.js prevents overlapping runs for the same issue/PR
job.js orchestrates one end-to-end run
server.js HTTP server: signature check, parse, classify, dispatch
app.js wires the above together
index.js process entrypoint, signal handling
test/ node:test unit + integration tests, no external services
```
Every module that touches the outside world (HTTP, git, subprocess) is
constructed with its dependencies passed in, so `test/` exercises the real
logic against fakes rather than a real Gitea instance or a real `claude`
binary.
## Running the tests
```
npm test
```
No dependencies to install — everything uses Node's built-in `node:test`,
`fetch` and `child_process`. Requires Node 20+.
## Configuration
All via environment variables, validated on startup (a bad or missing
value exits immediately with a clear error rather than failing later):
| Variable | Required | Default | Notes |
|---|---|---|---|
| `GITEA_URL` | yes | — | e.g. `http://gitea:3000` internally, or `https://git.example.com` |
| `GITEA_TOKEN` | yes | — | access token for the bot user, needs write access to target repos |
| `CLAUDE_CODE_OAUTH_TOKEN` | yes | — | from `claude setup-token` |
| `OWNER_LOGIN` | yes | — | only comments/issues from this Gitea username trigger a run |
| `WEBHOOK_SECRET` | no | unset | if set, requires a valid `X-Gitea-Signature` header |
| `TRIGGER_PHRASE` | no | `@claude` | |
| `CLAUDE_MODEL` | no | `sonnet` | |
| `CLAUDE_MAX_TURNS` | no | `25` | |
| `JOB_TIMEOUT_MS` | no | `1200000` (20 min) | |
| `PORT` | no | `3000` | |
| `LOG_LEVEL` | no | `info` | `debug` \| `info` \| `warn` \| `error` |
See `.env.example`.
## Deploying (Coolify, Docker Compose build pack)
1. Push this project to a Gitea repo (e.g. `selimaj-dev/claude-hook`).
2. In Coolify: **+ New Resource -> Public Repository**, build pack
**Docker Compose**, pointing at that repo.
3. Edit `docker-compose.yml`'s `networks.gitea-network.name` to the actual
Docker network your Gitea container is on
(`docker inspect <gitea-container> --format '{{json .NetworkSettings.Networks}}'`).
4. Set the environment variables listed above in Coolify's UI. Leave
**Domains** empty — this service has no public route by design.
5. Deploy, then confirm from Gitea's container:
```
docker exec <gitea-container> wget -qO- http://claude-hook:3000/healthz
```
6. In Gitea: Site Administration -> Integrations -> Webhooks -> **System
Webhooks** -> Add, URL `http://claude-hook:3000/`, trigger on **Issue
Comment** (and **Issues**, if you want `@claude` in a fresh issue body
to work too). To also support pull requests, enable **Pull Request**
(for `@claude` in a fresh PR description), **Pull Request Comment**
(general PR comments — routed the same as issue comments), **Pull
Request Review**, and **Pull Request Review Comment** (inline diff
comments). If you set `WEBHOOK_SECRET`, put the same value here.
7. Add `GITEA__webhook__ALLOWED_HOST_LIST=private` to Gitea's own
environment and restart it — Gitea refuses to call private addresses
otherwise.
## Logs
Every log line is one JSON object on stdout (or stderr for `warn`/`error`),
so `docker logs claude-hook` / Coolify's log viewer is directly greppable
or pipeable into `jq`. Key events to look for:
- `webhook received` — Gitea reached the service at all
- `ignored` with a `reason` field — why an event didn't trigger a run
- `trigger matched` / `job started` / `job finished` — a run went through
- `job errored` — something failed; the same message was also posted as a
Gitea comment on the issue
## Known limitations
- Runs execute inside this one long-lived container, not a fresh sandbox
per job — don't point `OWNER_LOGIN` checks loosely, and don't run this
against repos where you don't trust every collaborator who can comment.
- Uses your Claude subscription (via `claude setup-token`), so runs share
the same usage limits as interactive Claude Code / claude.ai use.