Before she opens a 48 MB Unreal asset, an artist runs one command:
grixel edit. The server hands her an exclusive lock on the
file. The art director then tries to lock the same asset and is refused,
with the holder's name in the error. That refusal is the feature: the
collision surfaced at the start of the day, before either had painted
anything worth losing. When the artist submits, the lock releases
itself. The transcripts are below.
Game studios are the people Perforce was built for. One depot holding code and art together. Atomic changelists. Exclusive locks so two artists never both lose a day inside the same un-mergeable binary. Workspace views so the audio team doesn't sync cinematics. No other version control system ever took studio problems that seriously, which is why, decades on, studios still run it.
For anyone who never ran a depot: imagine a shared company drive with perfect memory: every project lives at a path, every save is kept forever, certain folders have a lock on the door, and each person pulls down only the folders they need. Git, by default, photocopies the entire filing cabinet onto every laptop and reconciles the copies later; Perforce, and Grixel after it, keeps one cabinet at the center and hands you exactly the drawers you asked for.
Watching it age has been uncomfortable anyway. Review lives in a
separate product. p4 jobs stopped evolving around the time
bug trackers moved to the web. Federation is bolted on. And there is no
governed answer for the newest thing reading and writing files at your
studio: the AI assistant.
I've spent most of my career in games, much of it inside a Perforce depot. Grixel is my attempt to rebuild that shape from first principles: keep what studios rely on, replace what aged badly. The genuinely new part is an AI-context layer, which I'll get to at the end.
#Muscle memory, mapped
The Perforce vocabulary transfers almost directly. The depot is one
global, server-authoritative namespace; canonical paths look like
/companies/acme/games/seasonal/art/keyart_main.psd, and a
branch is just another path in that namespace. Workspace views become
grixel checkout with -path and
-exclude patterns, and grixel sync can widen
or narrow the view later. Changelists are still changelists: atomic,
submitted as a unit or not at all. Protections become a path-prefix ACL
file, with one large difference: denial means invisibility, not "access
denied". The +l filetype becomes a
lockable.yaml of extensions and paths that require a held
exclusive lock to submit. And p4 jobs becomes
grixel job, rebuilt the way I wish jobs had grown up.
The server daemon, grixeld, is authoritative for all of
it: metadata, ACLs, chunk storage, locks, review state, jobs. The CLI is
a thin client, so there is no policy a client can route around; clients
never held the enforcement logic in the first place.
#The choices and their costs
Server-authoritative is a choice with a cost: the server matters. If
it is unreachable you cannot submit, though a read-only offline mode
still answers grixel changes and local status. In exchange,
every invariant in this piece is enforced at one chokepoint no client
can bypass. Invisibility ACLs in particular are impossible in a
Git-shaped world where a full copy of the data sits on a contractor's
laptop.
On storage: metadata lives in a purpose-built Go substrate rather than an embedded database. Commits still serialize through a single writer, deliberately; that discipline is part of what makes changelists atomic. The substrate's job is to keep that critical section short: chunk upload happens before it, outside any lock, and only the metadata commit is serialized. What it replaces is lock-everything metadata contention, not write ordering, and the throughput ceiling is unmeasured. File content lives in a content-addressed chunk store on the local filesystem, split by FastCDC and hashed with BLAKE3, with an optional caching read proxy (shaped like p4p) for LAN fan-out. The server is one binary, no sidecars; a single operator can run it.
A housekeeping note: every command and output below is real, captured
against binaries built from main, in local mode against a scratch data
root. Commands carry -as alice or -as bob
because local mode asserts who is acting; in a team deployment you
grixel login once and the flag disappears. Long scratch
paths are shortened to ~/demo; nothing else in any output
is edited.
The cast is a small studio: alice, artist; bob, platform engineer; maya, art director.
· · ·
One depot, un-mergeable binaries, and the lock that saves the afternoon
The seasonal project keeps YAML spawn tables, config, dialogue, and multi-megabyte art in one ref, submitted with the same command. No Git LFS pointer files, no second storage system with its own quota and failure modes. Binary assets are the native case.
Two mechanisms make that livable. Chunk-level dedup: files are split
into content-defined chunks, each hashed, and only chunks the server has
never seen are stored or transferred. And the +l
inheritance: a PSD or an FBX cannot be merged, so the only honest
concurrency model is one person at a time, enforced by the system. An
admin declares once which types are un-mergeable:
$ cat data/.grixel/lockable.yaml
version: 1
extensions: [".psd", ".fbx", ".uasset", ".umap", ".wav"]
Alice submits the first pass of a 48 MB .uasset; the
type is lockable, so the submit claims the exclusive lock itself, just
long enough to land the change. (The base ... (advanced)
line is bookkeeping: her checkout advanced to the commit it just
made.)
$ grixel submit -as alice -m "first pass boss arena"
grixel: holding exclusive lock on /companies/acme/games/seasonal/art/boss_arena.uasset (lockable type)
Committed: 4afe1f546fc4a2bfd51b305cb3797c2c6abe620ae7af11b8fe92a28c7d0b8cfc
ref : companies/acme/games/seasonal
changes : 1
stored : 48.0 MiB new, 0 B deduped
parent : 0fc058b00345fb50caab34601935c49281e4c6b28b7a535fd853f4eb76a09898
base : 4afe1f546fc4a2bfd51b305cb3797c2c6abe620ae7af11b8fe92a28c7d0b8cfc (advanced)
Now the habit that matters. Alice is about to spend the morning in
that file, so she claims the lock before opening it, exactly like
p4 edit on a +l file:
$ grixel edit -as alice art/boss_arena.uasset
editing /companies/acme/games/seasonal/art/boss_arena.uasset — exclusive lock held (required for this type)
Maya, about to do the same, finds out immediately:
$ grixel lock -as maya /companies/acme/games/seasonal/art/boss_arena.uasset
grixel: lock /companies/acme/games/seasonal/art/boss_arena.uasset: locked by alice — run 'grixel locks /companies/acme/games/seasonal/art/boss_arena.uasset' for details
That is the refusal from the top of the piece, before any work existed. When alice submits, the lock releases itself and maya's next attempt succeeds:
$ grixel submit -as alice -m "boss arena: relight the pit"
grixel: holding exclusive lock on /companies/acme/games/seasonal/art/boss_arena.uasset (lockable type)
Committed: 2ef2f6e4c977591eb483a6cc4d4615a017528853ff9ee4994c2371155ac32698
ref : companies/acme/games/seasonal
changes : 1
stored : 19.8 MiB new, 28.2 MiB deduped
parent : 4afe1f546fc4a2bfd51b305cb3797c2c6abe620ae7af11b8fe92a28c7d0b8cfc
base : 2ef2f6e4c977591eb483a6cc4d4615a017528853ff9ee4994c2371155ac32698 (advanced)
$ grixel lock -as maya /companies/acme/games/seasonal/art/boss_arena.uasset # after the release
locked /companies/acme/games/seasonal/art/boss_arena.uasset (holder maya, id 6c43eb2a53ec5aeb)
Relighting the arena stored 19.8 MiB of new chunks against 28.2 MiB reused, with nothing configured per-file; big files chunk under a "large-asset" profile the chunker picks by size and extension.
Two honest details. Locks are enforced server-side at the submit
chokepoint, so a client that never heard of lockable.yaml
still cannot land a change to a locked path; the submit is refused with
the holder named. And auto-claim protects a forgetful artist from
clobbering anyone else's work; it does not hand back an afternoon spent
editing a file somebody else had claimed. The habit that saves the
afternoon is grixel edit first. Admins keep a break-glass
release for the lock someone took on holiday.
#Workspace views survive as sparse checkout
A Perforce client spec maps the subset of the depot you want on disk; it is how a 2 TB project fits on a 1 TB laptop. In Grixel the view is flags on checkout. Bob wants design without art:
$ grixel checkout -as bob -exclude 'art/**' companies/acme/games/seasonal sparse-ws
Checked out companies/acme/games/seasonal
dir : ~/demo/sparse-ws
base : 3c4bf58d08b850204e2140fa0a8fb1721468b5917536da5c9278280bd3639fcb
files : 2
view : all (excl. art/**) (partial checkout)
Edit files, then: grixel changes / grixel diff / grixel submit -m ...
$ du -sh sparse-ws
20K ~/demo/sparse-ws
The ref carries a 64 MB key-art PSD under art/; bob's
disk carries twenty kilobytes. To be clear: there is no virtual
filesystem paging content in behind your back. The view is explicit,
which also makes it predictable.
#Text still merges like text
Locks are for the un-mergeable; spawn tables, configs, and code take the ordinary 3-way path. When bob's working copy falls behind alice's submit on the same YAML file:
$ grixel sync -as bob
companies/acme/games/seasonal — synced 5294102c -> 698f1e7b
CONFLICTS (1):
design/spawn_table.yaml
resolve conflicts — 'grixel mergetool' (binary) or edit the markers + 'grixel resolve' (text) — then submit.
$ grixel resolve -as bob --mine design/spawn_table.yaml
resolved 1 path(s) (mine); staged for submit
grixel mergetool handles the same situation for binaries
by launching whatever tool the team registered for that extension,
P4Merge included.
#Branching, and what
replaces p4 integrate
Studios run mainline, dev, release. Each is a ref, a ref is a path, and creating one is a single server-side operation:
$ grixel branch -as bob companies/acme/games/seasonal companies/acme/games/seasonal-dev
Branched companies/acme/games/seasonal -> companies/acme/games/seasonal-dev @ b4fb8f15e9e0b83993fea60ee579510c309ee8f76c9434c67e29023ad821d1d9
Bob checks out seasonal-dev and submits an experiment
like any other ref:
$ grixel submit -as bob -m "dev: try softer spawn curve"
Committed: 20182c4de9f4613ca8a541e380083c9a4a2d367adcf2ea840b48ae204b9364da
ref : companies/acme/games/seasonal-dev
changes : 1
stored : 35 B new, 0 B deduped
parent : b4fb8f15e9e0b83993fea60ee579510c309ee8f76c9434c67e29023ad821d1d9
base : 20182c4de9f4613ca8a541e380083c9a4a2d367adcf2ea840b48ae204b9364da (advanced)
grixel merge is the p4 integrate role plus
the resolve: the server computes the common ancestor, the changes apply
into a checkout of the destination ref, conflicts use the same machinery
as above, and the result lands as an ordinary atomic changelist:
$ grixel merge-base -as bob companies/acme/games/seasonal companies/acme/games/seasonal-dev
merge-base of companies/acme/games/seasonal and companies/acme/games/seasonal-dev:
b4fb8f15e9e0b83993fea60ee579510c309ee8f76c9434c67e29023ad821d1d9
$ grixel merge -as bob companies/acme/games/seasonal-dev
Merging companies/acme/games/seasonal-dev into companies/acme/games/seasonal
base : b4fb8f15
src : 20182c4d
merged (1):
design/spawn_table.yaml
review, then: grixel submit -m "merge ..." (creates a merge commit)
$ grixel submit -as bob -m "merge dev spawn experiment"
Committed: 7e0d8dbf3160df362258fc07e21126cc6210a0f7148643c31da171370e82c365
ref : companies/acme/games/seasonal
changes : 1
stored : 0 B new, 35 B deduped
parent : b4fb8f15e9e0b83993fea60ee579510c309ee8f76c9434c67e29023ad821d1d9
base : 7e0d8dbf3160df362258fc07e21126cc6210a0f7148643c31da171370e82c365 (advanced)
Note the merge commit's storage line: 0 B new. The store already held every byte, so carrying and integrating branches is cheap.
· · ·
#Sign-off for art direction, inside the depot
Here is where Grixel goes past Perforce. Perforce's first-party
answer, Swarm, is a real review product; the seam is that it rides
beside the depot as a separate system with its own store of approvals.
Git hosting has required reviews too, and on a protected branch they do
block direct pushes. The seam there is that protection is hosting-layer
configuration: per-branch, off by default, with admin bypass toggles,
and the approval record lives outside the repository's object store, so
none of it travels to a mirror, a second remote, or a self-hosted bare
repo. Grixel builds review into the server that owns the content: a
gate.json committed at a path declares who must sign off
before content can be promoted to another ref.
$ cat gate.json
{
"version": 1,
"rules": [
{
"appliesTo": "art/**",
"target": "each-file",
"enforce": "gate",
"stages": [
{ "role": "art-director", "by": "group:art-directors", "order": 1 }
]
}
]
}
Maya promotes the working ref to the live one before signing off:
$ grixel review promote -as maya /companies/acme/games/seasonal /companies/acme/games/seasonal-live
BLOCKED /companies/acme/games/seasonal → /companies/acme/games/seasonal-live — 1 gate target(s) not approved:
none /companies/acme/games/seasonal/art/keyart_main.psd
$ grixel approve -as maya -m "key art matches palette; ship it" /companies/acme/games/seasonal/art/keyart_main.psd
approve recorded on /companies/acme/games/seasonal/art/keyart_main.psd
/companies/acme/games/seasonal/art/keyart_main.psd [content a724bb3c]
state: approved
[✓] art-director group:art-directors signed by maya
$ grixel review promote -as maya /companies/acme/games/seasonal /companies/acme/games/seasonal-live
OK promoted /companies/acme/games/seasonal → /companies/acme/games/seasonal-live @ cf131439 (1 gate target(s) verified)
The gate sits at promote rather than raw submit, so artists iterate freely on the working ref and the live ref only moves once coverage is satisfied. A sign-off also names the exact content revision it approved. When alice submits another pass of the key art, the approval goes stale on its own:
$ grixel review status -as alice /companies/acme/games/seasonal/art/keyart_main.psd
/companies/acme/games/seasonal/art/keyart_main.psd [content 590ee1ea]
state: none
[ ] art-director group:art-directors open — needs 1 of group:art-directors
Maya approved content a724bb3c; the file is now
590ee1ea; the gate reopened on its own. If your studio has
ever shipped an asset the art director approved three versions ago, you
know why sign-offs are content-addressed.
#Jobs, or
p4 jobs if it had kept evolving
I have a soft spot for p4 jobs: work tracked in the same
system as the files, linked to the changelists that resolve it. Grixel
rebuilds the idea: an admin-editable jobspec defines named types, each
with its own status machine and custom fields, and no baked-in
methodology vocabulary.
$ grixel job create -as maya -type bug -anchor /companies/acme/games/seasonal -title "Hero texture seams on consoles" -field severity=major -field platform=ps5 -assignee alice
note: "alice" is not a known principal — assignment is advisory, so it still applies
job000001 open (bug)
anchor : /companies/acme/games/seasonal
title : Hero texture seams on consoles
assignee : alice
platform : ps5
severity : major
created : 2026-07-05T23:32:40Z by maya
(That note line: assignee names are not validated against a user registry; assignment is an advisory label, so a bot or contractor name works. The changelist author is separate and comes from the verified login.) A submit cites the job the way a Perforce fix record would:
$ grixel submit -as alice --job job000001 -m "fix seams on hero texture"
grixel: holding exclusive lock on /companies/acme/games/seasonal/art/keyart_main.psd (lockable type)
Committed: 4bb92aee3ed1c442e92da6ecc482c0561231ca0631fdb91a4dfce77f90caf507
ref : companies/acme/games/seasonal
changes : 1
stored : 5.1 MiB new, 58.9 MiB deduped
parent : ad439db18fa79626577df2f8f35dd4225e7a131a325034bb135e1bc1731582bf
base : 4bb92aee3ed1c442e92da6ecc482c0561231ca0631fdb91a4dfce77f90caf507 (advanced)
jobs : 1 cited
Jobs nest into a freeform hierarchy with a progress rollup, and everyone gets a worklist:
$ grixel jobs -as alice --mine
assigned to me (1):
job000001 bug fixed Hero texture seams on consoles
available (unassigned) (1):
job000002 task open Spring festival content drop
$ grixel job show -as maya job000002
job000002 open (task)
anchor : /companies/acme/games/seasonal
title : Spring festival content drop
created : 2026-07-05T23:32:40Z by maya
children : 1 (fixed: 1)
job000001 fixed Hero texture seams on consoles
Jobs are event-sourced, so several actors edit at once without clobbering each other, and the history is a pollable feed for dashboards or automation:
$ grixel job events -as maya
2026-07-05T23:32:40Z job000001 created maya bug "Hero texture seams on consoles" [open]
2026-07-05T23:32:40Z job000002 created maya task "Spring festival content drop" [open]
2026-07-05T23:32:40Z job000001 link maya link parent job000002
2026-07-05T23:32:41Z job000001 link alice link commit 4bb92aee3ed1c442e92da6ecc482c0561231ca0631fdb91a4dfce77f90caf507
2026-07-05T23:32:41Z job000001 status alice → triaged
2026-07-05T23:32:41Z job000001 status alice → fixed
next cursor: 1783294361490152000
#The unannounced project
Every studio eventually has a project whose existence is the secret. Grixel's ACL model makes a harder promise than "access denied": if you lack read access, the path is simply gone. It never appears in a listing or a search result, and it is never echoed in an error. Bob can see both games; alice sees one:
$ grixel ls -as bob /companies/acme/games
/companies/acme/games/
seasonal/ -> ref @ 4ee43c8d
design/
readme.md
secret/ -> ref @ 6db73c65
pitch.md
$ grixel ls -as alice /companies/acme/games
/companies/acme/games/
seasonal/ -> ref @ 4ee43c8d
design/
readme.md
$ grixel ls -as alice /companies/acme/games/secret
grixel: not found: /companies/acme/games/secret
$ grixel ls -as alice /companies/acme/games/doesnotexist
grixel: not found: /companies/acme/games/doesnotexist
Alice's two errors are byte-identical, so she cannot even probe for existence. The invariant is structural: every metadata-returning handler routes through a single view-resolution chokepoint, and a model-based leak test asserts the less-privileged view never differs by a byte.
Because jobs are anchored to paths, they inherit the same rule. Bob files a job under the secret project; for alice it does not exist:
$ grixel job create -as bob -type task -anchor /companies/acme/games/secret -title "Nightjar vertical slice"
job000003 open (task)
anchor : /companies/acme/games/secret
title : Nightjar vertical slice
created : 2026-07-05T23:32:41Z by bob
$ grixel job show -as alice job000003
grixel: repo: job not found
No codename leaks through ticket titles; the tracker and the depot are the same governed namespace.
#The standards wiki, versioned and pinned
Studios keep brand guidelines and coding standards in a wiki that
drifts away from reality. Grixel makes them depot content that projects
depend on. The seasonal .grixel/scope.json maps two
subtrees from elsewhere in the namespace into the workspace:
$ cat .grixel/scope.json
{
"name": "SeasonalEvent2026",
"owner": "team:seasonal",
"description": "Spring festival content drop",
"mappings": [
{
"source": "/companies/acme/standards/brand",
"mountAt": ".standards/brand",
"pin": { "mode": "head" }
},
{
"source": "/companies/acme/standards/coding/go",
"mountAt": ".standards/go",
"pin": { "mode": "head" }
}
],
"ai_hints": "Tone: playful spring festival. Palette and voice live in .standards/brand."
}
grixel scope update resolves each mapping and commits a
scope.lock pinning the exact source revisions, so every
checkout reproduces the same standards. When bob later edits the brand
voice upstream, the project does not silently absorb the change; status
reports the drift and the team re-pins deliberately:
$ grixel status -as alice # after the upstream edit
ref : companies/acme/games/seasonal
base : 9d42cd83 (up to date)
changes : none (clean)
mappings : 2 total, 1 current, 1 drift-on-head
↻ .standards/brand (drift-on-head)
→ run 'grixel scope update' to refresh the mappings
#What the AI sees
This part is the capability Perforce never had a shape for.
grixel-mcp is a read-only MCP server that Claude Code
launches per workspace. It authenticates as you and passes through the
same ACL chokepoint, so an assistant's context is exactly what its user
is allowed to see, pinned to the revisions in scope.lock.
grixel mcp preview prints the assistant's entire session
view before you trust it; two excerpts from alice's tell the story.
First, the session's reach, which is exactly hers:
reach — file_tree(/), ACL-filtered (paths you can't see are simply absent):
//
companies/
acme/
games/
standards/
The secret project cannot appear in any listing this session
produces; the preview runs through the same chokepoint as
grixel ls. Second, the governed context bundle carries the
brand standards at the pinned revision:
<grixel:doc path="/companies/acme/standards/brand" revision="2b55b19b9de198035e7faf679dbd1060655eaad00ea522beb58532066b389369" pin="head" locked="true">
# Acme palette
Primary: #2B6CB0. Accent: #ED8936. Never use pure black.
# Acme brand voice
Playful, concrete, never snarky. Numbers over adjectives.
Address the player as 'you'; we never say 'users'.
</grixel:doc>
So an assistant drafting festival dialogue addresses the player as "you" because the pinned voice doc says so, not because someone pasted a wiki page into a prompt. The full preview, and the rest of the AI surface, is covered in Agentic freedom with guardrails.
There are no write tools on this surface. When an agent submits, it goes through the CLI write path under the same locks and gates as a human, with provenance recording the tool and model.
· · ·
#What Perforce still does that Grixel doesn't
Plainly, because this audience can smell evasion:
There is no P4V. There is no GUI at all yet; artists work in the CLI,
with grixel difftool and grixel mergetool
shelling out to P4Merge, Beyond Compare, kdiff3, or whatever a team
registers per extension. For plenty of art teams that alone rules out a
switch today. There are no Unreal or Unity plugins either, so nothing
like the checkout-from-the-editor flow the P4 engine plugins give you.
Both are obvious next steps, and neither exists yet.
There is no virtual filesystem. Sparse checkout is explicit: you declare a view and that is what materializes. Nothing pages in transparently the way a virtual drive would.
There is no shelving. Perforce can park an in-progress changelist on the server for a handoff or a machine swap; Grixel cannot, yet.
On scale, the biggest file in this piece is 64 MB. The chunker already selects the larger-chunk profile for big files and the mechanism does not change at 40 GB, but behavior at that size is unmeasured, so treat it as untested rather than promised.
On multi-site, the caching read proxy is the per-site read answer, the same shape as a p4p per studio, though its multi-node benchmark is unpublished and no multi-region mesh runs in production. There is no edge-server write answer yet: a remote studio's submits cross the WAN to the one authoritative server.
And if you have twenty years of history in a Perforce depot, I cannot migrate it for you today. Git import exists, both as a one-shot snapshot into a new project and as read-only vendoring pinned at a specific revision. Perforce migration tooling does not. And Grixel is pre-release.
#Where this leaves a studio
The pitch, compressed: your depot, your review tool, your bug tracker, and your standards wiki are today four systems with four permission models that drift apart, with AI assistants sitting across all of them and governed by none. Grixel collapses those into one namespace behind one chokepoint, so "allowed" means the same thing to an artist at the CLI, a CI bot, and an agent over MCP. That is what I mean by a system of record rather than a file store.
The Perforce shape is the anchor and the AI-context layer is the wedge. If you run a depot today, the muscle-memory mapping near the top is the part to argue with, and the concessions list is the part to price against your studio. Sign-off: the missing link goes deeper on review, and Building around agents on agent identity.
Grixel is pre-release. Read the docs, or request early access.