I came back to GitLab contributions after about 9 months away, and I had forgotten most of the process. So I wrote it down once, as rules and workflows a coding agent can follow. The agent does the busy work, and I approve every push and every comment.
In one hackathon week, this setup helped me open 20 MRs in 7 GitLab projects. 15 of them are merged now. The setup is public: gitlab-agent-kit.
Table of contents
- Getting started
- Make it yours
- Walkthrough with Antigravity CLI
- Results
- This is not an MR spam machine
- Other agents
- Changelog: what real use changed
- Takeaway
What is in the kit
-
AGENTS.md: the rules every agent follows. The main one is that nothing goes out without my OK. - 8 workflows:
onboarding,catchup,scout,analyze,claim,start,shipandfollowup. - 8 roles the workflows hand work to, like
issue-analyst,implementerandverifier. -
tools/gl.py: a GitLab API helper, so the agent does not write raw API calls. - Guards: a wording lint, git hooks, and a hook that asks before every push and every GitLab write.
- A playbook per project, with its commit format, its checks, and lessons from past reviews.
The loop is simple: find a free issue, analyze it, claim it, implement it, ship the MR, and follow up on reviews. The rules live in AGENTS.md, so it works with Claude Code, Codex, Cursor and Antigravity CLI (agy).
Setup
git clone https://gitlab.com/HamzaHassanain/gitlab-agent-kit.git
cd gitlab-agent-kit
tools/init.sh <your-gitlab-username>
Put a personal access token in .env, then run python3 tools/gl.py me to check it. .env and your notes are in .gitignore, so they stay on your machine.
Before the first run, make it yours:
- Fill the two
_edit me_spots inAGENTS.md: your stack and the projects you want to work on. - Rewrite sections 2 and 3 of
wording-instructions.mdfrom 10 to 20 of your own GitLab comments. This file decides how every comment and MR description sounds. - Add projects to
projects.jsonif you need more.
If you are new to GitLab, start onboarding at https://contributors.gitlab.com/start. The kit has an onboarding workflow for you too. The full guide is in the wiki.
One run with Antigravity CLI
The hackathon MRs went through Claude Code. For this post I ran the same flow in agy on glab, the GitLab CLI.
Scout
I asked for simple glab issues. It read the comments on each candidate and ranked 4 free issues.
Analyze
It started one issue-analyst per issue, in parallel. Each one writes a plan and a go or no-go. Then question-resolver answers the open questions from the code and history, so the claim does not ask things we can find ourselves.
Claim
The claim is one line: Hello! I'd like to work on this. The verifier checked it first, and the agent waited for my OK before posting.
Start and ship
4 implementer workers wrote the code and ran light checks only. Then mr-writer wrote each description from glab's template, with a real reproduction and real test output.
Ready for review
It checked the pipelines, showed me the exact @gitlab-bot ready comment, and asked before posting it.
The run opened 4 MRs: cli!4141, cli!4142, cli!4143 (merged) and cli!4144.
What the guard blocks
tools/agent_guard.py runs before every shell command agy wants to run. These are its answers for three commands:
$ git -C repos/cli push upstream main
{"decision": "deny", "reason": "Never push to upstream. Push only to origin (the community fork) on a feature branch."}
$ cat .env
{"decision": "deny", "reason": "Reading .env is not allowed. gl.py loads the token itself."}
$ git push -u origin my-feature
{"decision": "force_ask", "reason": "git push needs your explicit OK every time."}
Results
As of October 20, 2026, 15 of the 20 hackathon MRs are merged, 5 are in review, and none were closed. Every one of the 7 projects has a merged MR. 6 more MRs came out of review threads after that week, and 4 of them are merged.
Most review comments were not about the agent at all. Extend the existing test table instead of adding a new one. Add fields to the existing log line instead of a new line. These lessons are now in the playbooks, so the agent reads them next time.
This is not an MR spam machine
The main risk with an agent and a hackathon is a pile of low-effort MRs. So the kit pushes the other way:
- One issue per MR, small and focused.
- No taking someone else's issue. The check runs before starting and again before shipping, and
mr-createrefuses if the issue is not assigned to you. - Every MR has one honest line about AI use.
Some rules still depend on the agent following AGENTS.md. An agent with full shell access could go around them. So keep your agent's permission prompts on.
What real use changed
- Claim comments had plans and questions in them. Now a claim is one line.
- After the merges, I found 3 comments I never answered. Now
gl.py inboxlists everything waiting on me, and theverifierchecks every write. - A heavy local check ran out of memory on my laptop. Now the agent runs light checks only, and the pipeline does the rest.
Takeaway
The way I see it, the agent is good at busy work and bad at knowing when to stop. So I kept every outward action for myself. Try the kit, and open an issue there if something breaks with your agent.
I wrote this post with AI assistance. I reviewed it, and the numbers come from my real MR list.





Top comments (26)
The part that convinced me is that the guard is a hook, not a prompt suggestion: agent_guard.py answering deny on a push to upstream, and a verifier checking every write, is a very different trust level from "please ask me first" in AGENTS.md. The wording file built from your own past comments is the other detail people will copy — an agent MR that sounds like a template is exactly what makes maintainers suspicious. And the gl.py inbox born from finding 3 unanswered comments after the merges is painfully relatable. 15 of 20 merged with zero closed as unwanted says the restraint is working.
yes exactly.
prompts are suggestions, but hooks and guard scripts are hard limits.
if the agent can bypass a rule, it will do it at some point.
the wording file was necessary because maintainers spot generic AI text immediately.
and missing review comments was painful, so the inbox check had to be automated.
glad the restraint part made sense :)
That line is the one I'd underline: if the agent can bypass a rule, it will do it at some point. It's not even malice, it's just optimization pressure over enough runs — a suggestion in a prompt gets traded away the moment it conflicts with finishing the task, a hook can't be.
And the wording file point feels underrated. The guard stops the wrong action, but the wording file stops the contribution from reading as a drive-by in the first place — two different kinds of trust, and maintainers notice both. Automating the inbox check after missing review comments makes complete sense too: that follow-through is exactly what a human forgets under load, and exactly what shouldn't be left to memory.
when the agent is focused on finishing the task, a prompt is just a soft hint.
without a hook, it will always pick the path of least resistance.
and I agree on the two kinds of trust.
the guard prevents bad actions,
but the wording file prevents losing goodwill before anyone even reads the diff.
thanks a lot for the thoughtful reply :)
The least-resistance point is the one that stings, because it's not the agent being careless — it's doing exactly what it's optimized for: finishing the task. Maintaining the relationship just isn't part of that objective, so the goodwill layer can't be left to its discretion.
That's why I like your split of the two trusts. The guard protects safety: the bad action simply never happens. The wording file protects perception: how the work lands with the maintainer before a single line of the diff gets judged. And that second kind of trust is sneakier to lose — you usually only notice it's gone when a review turns cold, or doesn't happen at all.
One thing I'm curious about: is your wording file per-repo, or one global version? And when a repo's own conventions clash with the global one, which one wins?
one global file for the voice, it is how I write, so it does not change per repo.
but the repo's own format rules always win: the MR template, conventional commits, trailers, changelog format.
the wording file only covers the free text inside them.
anything repo specific goes in that project's playbook (projects/.md), not in the wording file.
That layering makes sense — voice as the constant, the repo's format as the contract, and the playbook as the local knowledge. The bit I like most is that the wording file only ever touches the free text inside the format, so it can never quietly override a trailer or a changelog convention. Curious how you keep projects/.md from bloating over time — do you prune it like code, or just let it accumulate?
it just accumulates for now, I do not prune it like code.
every lesson has the date and the issue it came from, so when a later review shows a line is wrong, the agent fixes that line.
and it is not as big as it sounds, the biggest playbook (glab) is 140 lines, most of them are 20 to 40.
if one gets too long I think I will split old lessons into a seperate file that is read only when needed.
That date-and-issue provenance is the part I'd steal — it turns a wrong lesson from a mystery into a fixable line, instead of something you have to re-argue from scratch. And 20–40 lines for most playbooks puts the size worry in perspective; the split-old-lessons-into-a-read-when-needed file idea sounds like a good pressure valve before the 140-line glab one ever becomes a problem.
I like that the review lessons ("extend the existing test table instead of adding a new one") go into the playbook rather than staying in your head. That's usually the stuff an agent forgets halfway through a long session. One thing I'd watch: if every lesson gets appended, the playbook becomes a fixed chunk of context on every request, so it might be worth pruning lessons once the agent reliably follows them. 15 of 20 merged with zero closed is a good result for a hackathon week.
Thank you!
good point about the context, but the playbook is not loaded on every request.
the agent reads projects/.md only when it works on that project, so the glab lessons never load when I work on gitaly.
pruning lessons the agent already follows makes sense, but I am not sure yet how to know it "reliably follows" one.
right now a lesson only gets removed or changed when a review shows it is wrong.
Ah, that's better than I assumed. If the project file only loads when you're working on that project, the cost is already scoped. On "reliably follows", one cheap signal is how many merged MRs have gone by since a review last had to flag that lesson. Once it's been quiet for a while you could move it to an archive section and see if the mistake comes back, and put it back if it does. Honestly your rule (only change a lesson when review shows it's wrong) is the safer default, I'd only bother with pruning once one project file gets long.
The exchange with eye_java is the part I keep coming back to: a guard keyed on command names leaves every other path to the token open. On my ticket board I went the other way. The agent has no side door; it uses the same APIs the frontend uses, with a token scoped to one board, and only I move a ticket to Done. The limit sits in the token, not in a list of commands. Which of your outward actions could the GitLab token itself refuse?
good question, some of the important ones are already refused by GitLab itself:
1- push to upstream, I am not a member there, only the community fork accepts my pushes
2- merging, only maintainers can merge
3- assigning myself to an issue, the API returns 403
so the hook on upstream push is a second layer, not the only one.
what the token cannot refuse: comments, reactions and opening MRs on any public project.
any logged in user can do that, so for those the gate is still the hook and me.
and a contributor cannot create a project access token on upstream, it needs the maintainer role.
so I like your setup more, the limit in the token is the better place, GitLab just does not give me that option yet.
@hamzahassanain0 that split is useful: GitLab already refuses push, merge and assign, so what is left is the write surface any account has. Comments and MRs are the two that reach other people with no review on GitLab's side. Does your hook hold a comment for your approval the way it holds an upstream push?
The guard returning
force_askon every push while flatly denyingcat .envis a really nice split, since it keeps the agent useful without handing it the keys. I also like that review feedback goes back into per-project playbooks; 'extend the existing test table instead of adding a new one' is exactly the kind of house style an agent can't infer from the code alone. Have you found the playbooks drift as maintainers change their minds, or do you prune them after each merged MR?Thank you!
I do not prune them after each MR. Every lesson has the date and the issue it came from, so when a later review or issue shows it is wrong, the agent fixes that line and adds "corrected ".
This already happened once in the glab playbook: a note about how
use_keyringis read was wrong, and work on another issue a week later fixed it.I think the real drift will show up later, when maintainers change their minds. The playbooks are only a few weeks old, so I am not sure yet.
The guard list is the part I would tighten next, because it is keyed on names rather than capabilities. gl.py is an authenticated GitLab client sitting in the repo next to the token in .env, so the hook sees the calls you routed through the documented path and nothing else. A token read through python3 -c instead of cat, or an API call made by whatever CLI the agent installs next week, produces the same remote write with no hook firing, since that is not the shape the rule was written against.
Two boundaries that cost less than the rules do: scope the PAT down to the projects in projects.json instead of an account-wide api token, and flip the network rule to default deny with python3 tools/gl.py as the only allowlisted egress, logging the method and URL of every call. That turns "I approve every push" into "I approve every remote write", which is the property you actually want - the push hook only proves the write left through the door you built.
you are right, I just tested it.
python3 -c reading .env, a raw curl POST to the API, and glab api are not denied by the guard.
they only fall back to the normal agy permission prompt, so the real gate there is me reading the prompt.
I said this in the post as the honest limit, but your framing is better: approve every remote write, not every push.
the default deny network with gl.py as the only egress is a good idea, I will try it.
about scoping the token, I am not sure GitLab lets a contributor limit a personal token to upstream projects yet.
for now I keep read_api for scouting and analysis, and use api only when I post.
15 of 20 merged with none closed is a better record than I get by hand on repos I barely know. I'm stealing the split between a flat deny on reading .env and force_ask on every push. I had both at the same level and caught myself approving prompts on autopilot. As the review lessons pile into each playbook, do you ever prune the old ones?
Thank you!
yes, the autopilot approvals are real, that is why .env is a hard deny and not one more prompt.
about pruning, no, I do not prune them. every lesson has the date and the issue it came from, so when a later review shows it is wrong, the agent fixes that line.
it already happened once in the glab playbook. the playbooks are only a few weeks old, so I am not sure yet how bad the drift will get.
The hook that asks before every push and every GitLab write is the part I'd copy first.
yes, that is the part I would keep if I had to drop everything else :)
it costs a few seconds per write, and I read every comment before it goes out.
The hook-not-prompt line matched a run I watched this morning. The prompt said count the segment first, then pull the rows; the agent ran both calls in parallel, because nothing stopped it, and on a 20-row run that costs nothing. On a 2,000-row run it is the bill. A hook that refuses the paid call until a count has been read, and a cap on what one call may spend, turned the suggestion into a limit. Same shape as your push guard: the rule lives where the agent cannot trade it away.
Worth adding that the "approve every push" part is the hard bit, and your wording-instructions.md trick is what makes it work. The rules only hold if the agent's output sounds like you, otherwise you're reviewing every comment twice — once for correctness, once for voice. Seeding that file from 10-20 of your own past comments is probably the highest-leverage part of the kit.
One gap I'd be curious about: the loop you describe is linear (scout → analyze → claim → start → ship → followup). What happens when the verifier says no-go on an issue you already claimed, or when a claim comment goes out and the issue turns out to be taken? Half the wasted agent time in my experience is in the edges around "claim" rather than the work itself, and those are exactly the paths that a rules file tends to under-specify since they're rare and annoying to describe.
Also: did you find a way to measure whether the agent is actually improving your throughput, or is 20 MRs in a week mostly an anecdote? A per-issue time-to-merge before vs after would be a much stronger claim than MR count.
Thank you, yes the wording file is a big part of it.
about the claim edges, you are right, this is where most of the mess is.
What the kit does now:
1- analyze runs before claim, so most no-go happen before I comment anything
2- the claim check runs again before the claim comment, before start, and before opening the MR.
mr-createrefuses if the issue is not assigned to me or someone else already has an open MR3- for an old claim, it asks the previous person first instead of taking the issue
What is missing: a no-go after I already claimed. There is no "unclaim" step yet, I think I will add one.
about throughput, honestly it is mostly an anecdote for now. I did not measure time-to-merge before and after.
What I have: 20 MRs in 7 projects during the hackathon week, 12 merged as of Oct 17, none closed.
I agree time-to-merge per issue is a better number, I may add it to the kit later.