🏠 Home ← Docs Index
Open Beta 📖 Interactive Reader Raw .md llms.txt

GitLab Webhook Integration

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:


Table of Contents

  1. Asking the agent for a hook
  2. Prerequisites
  3. 1-Minute Connection (CLI Wizard)
  4. Command Reference
  5. How It Works
  6. Events
  7. Usage Examples
  8. Security
  9. Troubleshooting
  10. Alternative Manual Setup

Asking the agent for a hook

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/&lt;key&gt;</b>"]
    GW --> SOP["<b>SOP Run on the Agent</b>"]

Before you start

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.

What you can say

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.

Listing, testing, removing

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).


Prerequisites

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

1-Minute Connection (CLI Wizard)

$ goclaw gitlab hook setup

The wizard will ask 5 questions and perform the following actions automatically:

Wizard Flow

  ╔════════════════════════════════════════════╗
  ║   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:


Command Reference

# 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 Example

KEY                 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     —                     —

How It Works

flowchart TD
    GL["<b>GitLab Project</b>"] -->|POST| EP["<b>/goclaw/hook/gitlab/&lt;key&gt;</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:&lt;agent&gt;:gitlab:&lt;object&gt;"]

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).

What the agent is told

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:

  1. A route for the exact key comes before a catch-all (a route of the type without a key).
  2. Among those, a stored route (registered via the Web UI or the CLI wizard) comes before a 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:

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.


Events

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

Usage Examples

1. Notify on Pipeline Failure

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."

2. Code Review on Push Events

push + merge_request events. The agent, with the gitlab tool:

3. Multiple Projects — Single Agent

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.


Security

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

Troubleshooting

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.

401 Unauthorized

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.

404 Not Found

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.

"no public URL available"

The wizard stops if gateway.ingress.url is missing. Add it to the config and try again.

Hit count remains 0

No events received from GitLab

  1. GitLab Settings → Webhooks → Recent events.
  2. The status of recent calls:
    • 200 ✓
    • 404 → see 404 above
    • 401 → see 401 above
    • 5xx → check lines matching [webhook] in the GoClaw log

Alternative Manual Setup

If 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.


Related Documents