Go-Claw agents can automatically process and respond to GitLab events such as push, pipeline, and merge request triggers.
There are three ways to connect a project:
gitlab_hook): an admin describes it in a conversation, and the agent
creates the project webhook in GitLab and the route in GoClaw, each event running a SOP. See
Asking the agent for a hook.goclaw gitlab hook setup wizard: five questions in a terminal configure both sides.An admin asks in a conversation, and the agent's gitlab_hook tool does the rest: it creates a
webhook in the GitLab project for the events you name, and the route /goclaw/hook/gitlab/<key>
in GoClaw. Each of those events then runs a SOP with the event as its input. Removing the hook
removes both.
flowchart LR
GL["<b>GitLab Project Webhook</b><br/>Only the events asked for<br/>X-Gitlab-Token = route secret"] -->|"merge request / pipeline / push âĶ"| GW["<b>/goclaw/hook/gitlab/<key></b>"]
GW --> SOP["<b>SOP Run on the Agent</b>"]
| What | Where |
|---|---|
The GitLab instance, with allow_write: true |
tools.gitlab (or tools.gitlabs.<name>): the Integrations page or config.yaml |
A token with the Maintainer (or Owner) role on the project and the api scope |
the instance's token |
| An address GitLab can reach the gateway at | gateway.ingress.url, and a listener there |
| A SOP for the events to run | the SOPs page, or the agent's sop_save tool |
The GitLab host needs no entry on tools.http.allow: the GitLab tools may reach the instance you
configured.
GitLab and local addresses. GitLab refuses a webhook to a loopback or private address
(10.x, 192.168.x, a name without dots) unless its administrator allows it: Admin âš Settings âš
Network âš Outbound requests âš Allow requests to the local network from webhooks and
integrations, or the host on the allowlist below it. The plan says so when the gateway's
address is such an address; GitLab's refusal is passed on with the same advice.
Make merge requests in group/backend run the mr-review SOP.
Pipelines on group/backend should run pipeline-triage. Call the hook "Backend pipelines".
Run release-notes on pushes to release/* in group/app.
The agent answers with a plan, and nothing is created until you confirm it in your next message:
Plan 3f9a1c20 â nothing is created yet.
GitLab "default" (version 17.5.1-ee), project group/backend, gets a webhook "Backend MR review":
- on merge_request events
- GitLab calls https://bot.example.com/goclaw/hook/gitlab/backend-mr-review, with a new secret only this route knows (X-Gitlab-Token)
GoClaw: each event runs SOP "mr-review" (MR review) as agent main.
Events: merge_request, pipeline, push, tag_push, issue, note, job, deployment,
release, wiki. branch_filter (a wildcard such as main or release/*) narrows push events.
Confidential issues and comments are never sent.
The plan is refused, with the reason, when the project does not exist for the token, the token's role is below Maintainer, the project already has a webhook to that route, the route key is taken, or the instance does not allow writes.
Only a human admin can do this, in a conversation of their own: never a SOP, cron, webhook or
subagent run, whose inputs are text anyone can write. The gitlab tool itself only reads hooks
(hooks_list, hook_get).
For the wizard to work, the following 3 items must be configured in config.yaml:
# 1. Gateway public HTTPS URL â destination address for GitLab webhooks
gateway:
ingress:
type: "proxy" # or "nginx" / "ngrok"
url: "https://bot.example.com"
# 2. GitLab host + PAT (api scope) â used by the wizard to call the GitLab API
tools:
gitlab:
url: "https://gitlab.com" # or your self-hosted instance
token: "${secret:GITLAB_PAT}"
allow_write: true # only for the agent's own writes (comment_add, âĶ); the wizard does not need it
# 3. Default agent (router/main) â receives events
Store the PAT in the vault:
$ goclaw secret set GITLAB_PAT glpat-xxxxxxxxxxxxxxxxxxxx
$ goclaw gitlab hook setup
The wizard will ask 5 questions and perform the following actions automatically:
ââââââââââââââââââââââââââââââââââââââââââââââ
â GoClaw GitLab Webhook Setup Wizard â
ââââââââââââââââââââââââââââââââââââââââââââââ
ââ Step 1: Gateway Public URL ââ
Gateway public URL [https://bot.example.com]: âĩ
ââ Step 2: GitLab Instance ââ
GitLab host [https://gitlab.com]: âĩ
â Using PAT from tools.gitlab.token
â Connected to GitLab 16.9
ââ Step 3: Project ââ
Project path (e.g. group/repo): group/payments-api
â Project found: group/payments-api (ID: 42)
ââ Step 4: Events ââ
Select events (space-separated numbers, or 'all'):
[1] push [2] tag_push [3] merge_request
[4] pipeline [5] issue [6] note
[7] wiki [8] deployment [9] release
Choice [1 3 4]: âĩ
â Selected: push, merge_request, pipeline
ââ Step 5: Target Agent ââ
Which agent should receive events? [main]: âĩ
ââ Step 6: Creating Webhook ââ
â Webhook URL: https://bot.example.com/goclaw/hook/gitlab/group-payments-api
â Secret: glsec_a7b3âĶ (auto-generated)
â Webhook created in GitLab (hook_id=123)
â Route registered in GoClaw (db_id=5)
â Secret stored in vault as GITLAB_WH_GROUP_PAYMENTS_API
â
Setup complete!
Notes:
[1 3 4] corresponds to the numeric options push, merge_request, pipeline. You can type all to select all events.goclaw secret get GITLAB_WH_<KEY> prints it later (here goclaw secret get GITLAB_WH_GROUP_PAYMENTS_API); goclaw secret list shows it masked.# Register a new webhook (wizard)
goclaw gitlab hook setup
# List all GitLab webhooks in a table
goclaw gitlab hook list
# Send a test event via GitLab API
goclaw gitlab hook test <key> # default: push_events
goclaw gitlab hook test <key> --trigger issues_events
# Remove
goclaw gitlab hook remove <key> # deletes from both sides
goclaw gitlab hook remove <key> --keep-route # deletes only from GitLab
goclaw gitlab hook remove <key> --keep-gitlab # deletes only from Go-Claw DB
list Output ExampleKEY AGENT EVENTS ENABLED HITS LAST HIT ERROR
group-payments-api main push,merge_request,pipeline yes 42 2026-04-15 14:23 UTC â
backend-api devops pipeline yes 8 2026-04-15 09:10 UTC â
frontend-web main push no 0 â â
flowchart TD
GL["<b>GitLab Project</b>"] -->|POST| EP["<b>/goclaw/hook/gitlab/<key></b><br/>GoClaw gateway"]
EP --> RT["<b>Webhook Router</b><br/>1. Verify X-Gitlab-Token<br/>2. Check if event matches filters<br/>3. Add summary to agent session<br/>4. Return 200 OK once the event is taken"]
RT --> BG["<b>Agent Processes in Background</b>"]
BG --> SESS["<b>Saved in Session</b><br/>webhook:<agent>:gitlab:<object>"]
The events of one object share a session: webhook:<agent>:gitlab:<object_kind>-<id>
(for example merge_request-1234, with the object's ID, not its !42 number), or
proj-<project id> for an event about no single object, such as a push. A comment belongs
to what it hangs off: a note on an issue is in issue-<issue id>, a note on a merge request
in merge_request-<id>, and a work item's events are in issue-<id> too, so an issue's edits
and its comments share one session. A route that runs a SOP logs what started its runs in
webhook-sop:<agent>:gitlab:<sop>:<object> instead, and the runs of one object take turns
(see Runs started by a webhook).
An agent driven by a webhook sees a summary, not the raw payload. When the route is bound to a SOP, each step of its run is shown the JSON as data from outside â in a section marked untrusted, never as the SOP's own context â up to 16 KB of each event and 48 KB in all. The summary names the object, so a skill does not have to go looking for it:
ð Webhook received: type=gitlab key=group-payments-api event=merge_request
Project: group/payments-api
MR: !42
Title: Add rate limiting to the refresh endpoint
Action: update
State: opened
Draft: yes
Branches: feat/rate-limit â main
SHA: a1b2c3d4
URL: https://gitlab.example.com/group/payments-api/-/merge_requests/42
Labels: Status::Review required, Type::Feature
| Event | What the summary carries |
|---|---|
merge_request |
MR iid, title, action, state, draft flag, source â target branch, last commit SHA, URL, labels |
issue |
issue iid, title, action, state, labels |
note |
which MR or issue the comment hangs off, and its author |
pipeline |
pipeline id, status, ref, SHA, and the MR iid when the pipeline belongs to one |
push / tag_push |
ref, checkout SHA, commit count, who pushed |
The SHA is shortened to 8 characters â the form used in comment markers and in
git log --oneline. An event whose object_kind is not modelled falls back to
printing ref and message.
Route Lookup Precedence:
config.yaml webhooks: route.A paused route keeps its deliveries: they get 404, not the catch-all. Every key a route has
(sop_id, batch_mode, token, âĶ) is in Webhook Routes.
What the sender is told:
| Status | When |
|---|---|
200 |
The event was taken: a SOP run started (or queued, for a bulk route), or the agent will answer it. {"status":"filtered"} when the route's event filter leaves it out, {"status":"duplicate"} when the route took this delivery before. |
401 |
The token or signature does not match the route's secret. |
403 |
The sender's address is not in gateway.ingress.allow_ips (see Security). |
404 |
No route for the type and key, or the route is paused. |
409 |
The route's SOP cannot run: it was deleted or disabled, or it is overdue for review. On a route without a SOP, the route's agent is not running on this gateway: it is not in agents.list, was added since the gateway started, or its model failed to start. |
500 |
The SOP run could not be started; send it again. |
413 |
The payload is larger than 4 MB. |
429 |
With gateway.rate_limit on, the sender's address made too many requests in a minute (Retry-After: 60). |
503 |
Too many deliveries are in progress (Retry-After: 30), another request is taking this same delivery right now, or the delivery's ID could not be recorded (both Retry-After: 10). |
A delivery sent again runs once. For a week, the route remembers the ID the sender gave each delivery it took:
Idempotency-Key (GitLab 17.4 and later; it stays the same when GitLab retries),
else its X-Gitlab-Event-UUID;X-GitHub-Delivery;Idempotency-Key header from any other sender.A delivery with an ID the route took before is answered {"status":"duplicate"} and runs
nothing. A delivery the route did not take (401, 409, 500, 503) runs when it is sent again (a
503 for a delivery another request was taking is a repeat or runs, as that request ended), and
so does one it took and then dropped without running it, as the route shows on the Webhooks
page (no agent came free in time, a bulk batch whose SOP refused to run). On a route to every
agent, a chat delivery runs again only if no agent ran it, and a bulk one does not.
On a route that runs a SOP, a restart does not change that. A delivery counts as taken only once
what it started is on record: its SOP run, or its events on a bulk route's queue. One the gateway
was taking when it stopped is settled when it starts again: a resend runs, unless its SOP run had
begun a step (or its events are queued); on a route for every agent, the resend runs only the
agents whose run had not begun. A bulk batch is marked with its run before the run starts and
stays on the queue until the run begins a step, so a batch whose run had begun is not run a
second time after a restart, and one whose run had not â still waiting for a free SOP slot when
the gateway stopped, or until the wait timed out â runs. A single delivery whose SOP run was
still waiting for a slot was answered 200, so its sender does not send it again, and it is
lost. A route without a SOP hands the delivery to the agent's turn, which is not on record:
a turn still waiting or running when the gateway stops is lost.
Every refusal a route answers, and an event dropped after it was taken (no worker came free in
time), is shown next to the route on the Webhooks page, except the 503 for a delivery another
request is taking. A 403, 413 or 429 is answered before any route is looked up, a 404 for an
unknown key has no route to show it on, and a route in config.yaml records nothing there:
look for those in the gateway log. Deleting a route set up by goclaw gitlab hook there
also removes its webhook in the GitLab project, or says why it could not.
CLI event name â GitLab API field â Incoming X-Gitlab-Event header. A route's event filter
can use any of the three spellings: they are compared as the CLI name, so merge request,
Merge Request Hook and merge_requests_events all mean merge_request.
| CLI Name | GitLab API Field | X-Gitlab-Event Header |
|---|---|---|
push |
push_events |
Push Hook |
tag_push |
tag_push_events |
Tag Push Hook |
merge_request |
merge_requests_events |
Merge Request Hook |
pipeline |
pipeline_events |
Pipeline Hook |
issue |
issues_events |
Issue Hook |
note |
note_events |
Note Hook |
wiki |
wiki_page_events |
Wiki Page Hook |
deployment |
deployment_events |
Deployment Hook |
release |
releases_events |
Release Hook |
goclaw gitlab hook setup â select only the pipeline event. Add the following to the agent's system prompt:
"When a pipeline event is received from the webhook: if
status=failed, immediately raise an alert, include the assignee's name, and summarize the failed job's logs."
push + merge_request events. The agent, with the gitlab tool:
mr_changes actioncomment_add action (needs allow_write: true on the GitLab instance)Run setup once for each project. They will be registered in the database as follows:
KEY AGENT EVENTS
backend-api devops pipeline
frontend-web devops pipeline
infra-terraform devops merge_request,pipeline
All events route to the same agent. If you set agent=*, they will be broadcast to all agents.
1. Random secret + vault. The wizard generates a 16-byte glsec_* secret and stores it in the encrypted vault. No plain-text secrets are stored in the configuration file.
2. HTTPS enforced. The wizard rejects any URL that does not start with https://. By default, GitLab also rejects plain HTTP webhooks.
3. Event filtering. By excluding unnecessary events from the database events field, you:
4. GitLab IP allowlist (optional). Restrict requests by adding GitLab.com's IP range to gateway.ingress.allow_ips:
gateway:
ingress:
allow_ips:
- "34.74.90.64/28" # GitLab.com SaaS
- "10.0.0.0/8" # On-prem
allow_ips replaces allow_preset: with a list here, a preset such as microsoft-teams no
longer applies, so add the addresses of every other sender that reaches the gateway this way.
Requests from the gateway's own machine (loopback) are always let through.
5. Secret rotation (every 3-6 months):
$ goclaw gitlab hook remove group-payments-api # clean up the old one
$ goclaw gitlab hook setup # recreate with a new secret
Start where GitLab shows the answer: Settings â Webhooks â Recent events, then the request's Response body. GoClaw says there why it refused a delivery, and records the same reason on the route: the Health column of Automation â Webhooks shows it.
The response names the cause:
| Response says | Cause | Fix |
|---|---|---|
the X-Gitlab-Token header is missing |
The GitLab webhook has no Secret token. | Edit the webhook in GitLab and enter the route's secret as its Secret token. |
X-Gitlab-Token does not match this route's secret |
GitLab sends a different secret. | Enter the route's secret in GitLab, or set the route's secret to what GitLab has. |
this route has no secret |
The route was saved without one. | Edit the route and enter a secret (or create it again: an empty secret is generated). |
âĶa ${secret:âĶ} reference with no value in the vault |
The route's secret points at a vault key that does not exist. | Add the key with goclaw secret set, or enter the secret itself. |
One route answers one secret. A route with no key answers every
/goclaw/hook/gitlab/<anything> URL, so every project that uses it must send that same
secret. To give projects their own secrets, create a route per project with its key.
| Response says | Cause | Fix |
|---|---|---|
no active webhook route for gitlab/<key> |
A route matches, but it is paused. | Enable it on the Webhooks page (âķ). |
no webhook route for gitlab/<key> |
No route has that key. | Check the key in the URL; the gateway log lists the keys that exist. |
| An HTML page, or nothing from GoClaw | The request reached the Web UI port, not the webhook listener. | When gateway.ingress.port is set, webhooks are served only on that port. The Webhooks page shows both ports. |
webhook_secret_slug only applies to channel webhooks (/hook/<slug>/msteams), not to
/goclaw/hook/âĶ routes.
The wizard stops if gateway.ingress.url is missing. Add it to the config and try again.
200 {"status":"filtered"} and not counted.200 â404 â see 404 above401 â see 401 above5xx â check lines matching [webhook] in the GoClaw logIf you cannot run the CLI wizard (e.g., headless server without SSH access):
A. Register the route on the Go-Claw side â via the WebUI or in config.yaml:
WebUI â Automation â Webhooks â + New Webhook (Type: gitlab, Key, Agent, Events). Leave Secret empty to have one generated: after saving, the page shows the URL and the secret to enter in GitLab, and the secret is not shown again. When you edit a route later, an empty Secret field keeps the current one.
Or in config.yaml:
webhooks:
- type: gitlab
key: myproject
secret: "${secret:GITLAB_WEBHOOK_MYPROJECT}"
agent: main
events: [push, pipeline, merge_request]
The gateway reads the change when it reloads config.yaml; no restart is needed. Save the secret using the goclaw secret set command.
Every route key â sop_id to run a SOP per event, batch_mode: bulk and batch_window to run one
for a window of events, token â is described in Webhook Routes.
Routes for other senders work the same way; for Graylog alerts, see
Graylog Alerts That Run a SOP.
B. Register the webhook on the GitLab side â from the GitLab UI:
GitLab Project â Settings â Webhooks:
| Field | Value |
|---|---|
| URL | https://bot.example.com/goclaw/hook/gitlab/myproject |
| Secret Token | (Must exactly match the secret configured on the Go-Claw side) |
| Trigger | Push / Merge / Pipeline (select what you need) |
| Enable SSL verification | â |
â ïļ The secret must be exactly the same on both sides. A mismatch of even a single character will cause a 401 response for incoming calls from GitLab. The CLI wizard handles this alignment automatically.
webhooks: keys/api/webhooks and the receiver's status codesgitlab tool actions