Notes from building an AI design collaborator · part 16
Six designers, one GitHub account, and a script that saves us from ourselves
· 7 min read
Six of us on the design team share one GitHub account. That's a story for another post (seats cost money and ours were full), but the short version is we all push to the same repo as effectively the same user. Which works fine right up until two people push work that stepped on each other, and then it's not fine at all.
The safe habit is boring and well known. Pull before you push. Get the latest, then send yours. Everyone knows it. I told everyone to do it. I wrote it in the onboarding notes.
And then one afternoon, busy and distracted, I pushed without pulling first.
The habit fails exactly when you need it
Nothing broke that time, I got lucky, it was a clean fast-forward. But it made the problem obvious. The rule is real, the rule is simple, and the rule depends on me remembering it in the exact moment I'm least likely to, which is when I'm heads-down and in a hurry. A safety rule that only works when you're calm and paying attention isn't a safety rule. It's a wish.
If I could forget it, having literally written the reminder, then everyone could forget it. Adding a louder reminder would just be a louder thing to forget.
Move the rule out of people's heads
So I stopped trying to make people remember and made the machine enforce it.
Worth being precise about what kind of machine, because I've described this loosely before and it matters. This is not a git hook. It's a hook in the AI assistant, the kind that runs before a tool call and can refuse it. It watches for a push, fetches origin first, and if your branch is behind it blocks the push and says so. The rule doesn't live in anyone's memory anymore. It lives in the plumbing, and the plumbing doesn't get distracted.
That choice has a consequence I should say out loud. It guards pushes the assistant makes, which is
almost all of them here, because almost nobody on this team runs git by hand. But a designer who
opens a terminal and types git push walks straight past it. The proper backstop for that is
branch protection on the server side, which needs an org owner and is still pending. So this is the
layer I could actually build, not the complete answer.
It ships with the project rather than living on my machine, so a teammate gets it by cloning, with one caveat worth knowing if you build the same thing. The first time you open the clone, the app asks you to approve the project's hooks, and until you do, the guard isn't running. So it isn't quite "nobody has to opt in." It's "everybody opts in once, at the start, instead of remembering something every single push," which is a trade I'll take.
Detecting a push turned out to be fiddlier than expected, incidentally. It has to catch the plain
command, the one with a directory flag in front, the one chained after a cd, and the forced
variant, while not tripping on the word "push" sitting inside a commit message. And when it can't
verify anything, offline, no upstream, not a repo, it lets you through. A guard that blocks
legitimate work gets switched off within a week.
My first version told people to do the forbidden thing
The guard blocks you and prints what to do next. Mine said to run a rebase.
We have a rule against rebasing here, and it's not aesthetic. A rebase replays your commits on top of theirs, and when it collides it stops halfway, writes conflict markers into a designer's files, and leaves git in a state that needs someone who knows git to get out of. For a team of designers, that is a strictly worse outcome than the stale push I was preventing.
So the message got fixed. It points a session at the safe pull, points a by-hand pusher at a fast-forward-only pull, and for the genuinely diverged case, where no single command is safe, it deliberately gives no command at all and names a person to go ask instead. Handing someone a command that can strand them is exactly what the guard existed to prevent.
The other half, which matters more
The guard stops a bad push. It does nothing about the work that never gets pushed at all, and that turned out to be the real problem. In one week: commits sitting unpushed three separate times, two catch-up merges absorbing a pile of accumulated remote work, and a designer's ten-file wireframe round found on a local branch that existed nowhere but one laptop.
So the assistant commits and pushes as part of the work now, and the whole design follows from one observation. The failure modes are asymmetric. Not pushing risks losing work irreversibly. Pushing risks a conflict, which is recoverable. So lean hard toward pushing, as long as a conflict stops the machine rather than being resolved by it.
Everything else is a consequence of that line. Commit at every step gate and push immediately after, not at session end, because designers don't end sessions, they close laptops, and an unpushed commit is still unbacked-up. Stage by explicit path, never everything, because the folder holds other people's in-flight work in the same clone. On a conflict, commit locally, tell the designer once in plain words, and stop pushing for the rest of the session. Never rebase, never force.
The bug that taught me the most
Sessions started creating branches. Not because anything told them to, but because "if you're on the main branch, make a branch before committing" is sensible default behaviour for an AI working in a repo, and nothing in our setup overrode it.
Here's what that did. Our pull and push helpers work through the current branch's upstream. A freshly created local branch doesn't have one, so both calls exited on their "nothing to do here" path. One quiet line, no error.
The moment a branch appeared, the safety net switched itself off. Silently, and precisely in the situation where the work was least protected. That ten-file wireframe round I mentioned was sitting exactly there.
The fix was a written branch policy, and the thing I'd stress is that the policy explains itself. A bare "don't create branches" invites an agent to conclude it knows better, and in most repositories it would be right. So it carries the four reasons this repo is different, including that finding. A rule with its reasoning attached survives contact with something smart enough to argue.
One more, because it's funny and it isn't. An agent testing a fix to the push helper ran the script from its real path, and the script resolves its own repo root rather than using yours, so it promptly pushed five unreviewed commits to main. Harmless as it happened, a clean fast-forward, nothing overwritten, and completely unreversible without the force-push we forbid. Test this class of thing against a disposable clone. The tools that exist because people improvise around the rules are the exact tools someone will test by just running them.
If a rule actually matters, don't write it in a doc and hope. A doc is a reminder, and reminders fail precisely when you're too busy to read them, which is the same moment you were going to make the mistake. Encode it somewhere it can't be forgotten, then check what happens to your encoding in the situations it didn't anticipate.
Careful is not a strategy. Six tired people sharing one account taught me that in one afternoon.
Part of a series on building an AI collaborator for our design team at Xflow. Each post stands on its own.
- design-ops
- git
The work behind the series
Designing the Instructions
This post is one thread out of a three-month project: an AI design collaborator for a payments team. The case study is the whole of it: what worked, what broke, and what is still unproven.
Read the case study →