Binary assets cannot be merged. Two artists editing the same .psd or the same Unity scene means
one of them loses work, and locking is the only mechanism Git offers to stop that happening. It is
the difference between LFS being usable for a game project and being a hazard.
git lfs lock Assets/Scenes/Arena.unity
git lfs locks
git lfs unlock Assets/Scenes/Arena.unity
A lock belongs to the identity behind the token, resolved from the forge, so git lfs locks names
the person to go and talk to. Taking a lock someone else holds is refused with their name attached,
rather than silently overwritten.
Only the owner can release a lock. Anyone else needs --force, and force needs admin rights on
the repository, the same person who could rewrite the branch anyway.
LFSX_LOCK_MAX_AGE is the answer to the same situation without an administrator in it. Unset, a
lock lasts until someone releases it, which is what happened before this existed. Set, a lock nobody
has touched for that long can be taken by anyone who could have taken it in the first place:
LFSX_LOCK_MAX_AGE=1209600 # two weeks
A stale lock is not deleted, it is taken. Until somebody claims it, it is still listed and still names its holder, because the useful answer is not "this is free" but "marie had this and has not touched it in three weeks". The takeover is recorded in the log with the previous owner, the new one, and how long it had been.
The clock runs from when the lock was taken. Last-touched is closer to what people mean by stale, and it would mean guessing which object a path maps to; the claim is the thing this server can answer for.
One honest limitation: git lfs locks cannot show you any of this. The protocol's lock is an id,
a path, a timestamp and an owner, with nowhere to put a "stale" flag, so no phrasing of the JSON
would make the client display it. The repository page shows it, which is where somebody goes to ask
why they cannot take a scene.
Two bounds guard the store, sized so that no real workflow meets either. A lock path is capped at
4096 bytes, far above anything git itself accepts, and a repository holds at most 10,000 locks:
the eleventh thousand is what a scripting loop produces, not a studio. The dashboard shows the
first fifty and counts the rest; git lfs locks pages through everything.
Locks live next to the objects, under .locks/, so they are covered by the same backup and
disappear with the repository. That means $LFSX_STORAGE_ROOT/.locks/ on a volume and the same
prefix in the bucket when objects are in one: whatever holds the objects holds the locks, because a
second replica has to agree with the first about who is holding what.
What makes a lock unique
Only one of two clients reaching for the same path may come away with it, and the store is what
decides which. On a volume that is create_new, a filesystem primitive that either makes the file or
does not. In a bucket it is If-None-Match: *, a conditional write the store has to refuse when the
key is already there.
Not every S3-compatible implementation performs that condition. AWS added it in August 2024 and MinIO has it; some of the others accept the header and write anyway, answering success twice. Both callers are then told the lock is theirs, nothing detects it, and locking has quietly become advisory, which is exactly the situation it exists to prevent.
So the server asks at startup rather than assuming. It writes one key twice under the condition and
requires the second to be refused. A store that performs both writes, or that cannot be asked, loses
locking: taking a lock answers 501 with a message saying why, and objects, transfers and everything
else carry on untouched. A refusal at the moment somebody asks is the only honest answer available;
handing out a lock two people can hold is not.
This is why there is no table here of which providers work. Each deployment establishes it against its own store, at boot, and says so in the log, which is worth more than a list somebody tested once.