Sharing a workspace with a team
Nothing about a workspace repo is single-user. Any number of engineers clone the same one, through the load flow of getting started:
/s:workspace clone git@github.com:acme/myapp.git ~/workspaces/myapp
Members are machine-local, always. Each clone runs its own sync ladder and
picks its own rung per member: a worktree, a --reference clone, or a full
clone. The manifest records only where a member comes from (url), never
how it landed, and neither engineer commits or shares the rung.
The shared surfaces are the knowledge, not the code. The repo tracks:
- the manifest (
.shipd-config.json) — members, focus, and anystore_root, - the wiki (
.shipd/wiki/) — pages plus theindex.mdcatalog, - the queue (
.shipd/wiki/queue.md) — pending oracle questions, which any teammate answers, - the initiatives and project context (
.shipd/initiatives/,.shipd/projects/).
Ordinary git carries them. git pull brings a teammate's pages, answers, and
briefs into your clone; git push publishes yours. There is no shipd server,
no sync service, and no workspace-level daemon.
The ignore block does not churn. The engine derives the managed members
block in .gitignore from the manifest's member paths, between
# >>> shipd-workspace members and # <<< shipd-workspace members. Every
clone with that manifest reconciles to the same block, so two machines running
shipd workspace sync --write-gitignore produce no diff to fight over.
Inherited worktree hooks need your consent
post-worktree-scripts resolve nearest-wins through the layered config, so a
list in the workspace repo's tracked .shipd-config.json governs every member
repo beneath it. One registration seeds every member's fresh worktree — and
whoever last pushed wrote shell commands your next shipd worktree runs
unasked.
The receiving machine holds the switch. The engine keeps a machine-local trust
ledger at ~/.shipd-trust.json, keyed by a fingerprint of the exact command
list you consented to. The key is the list, not the file carrying it, so
consent travels with the commands. Trust for a tracked config covers the
worktree's own copy of it, and every other clone here. Before any list runs:
- a trusted list runs silently, exactly as it always has;
- an untrusted list on a terminal prints the declaring config file and every command, then asks before running anything;
- an untrusted list with no terminal refuses — an autopilot run, CI, any
non-interactive session. The worktree stays in place, no hook runs, and the
verb exits
3.
Grant consent explicitly with shipd worktree hooks trust, which prints the
resolved list and its source, records the entry, and exits 0. shipd worktree hooks run then finishes the parked setup. Run it from inside the
worktree the refusal names, never at the parked root.
Registering a hook through shipd worktree hooks add (or /s:worktree-hooks)
trusts the list it writes, so your own hooks never prompt. It holds only where
you already trusted the effective list there, or that list was empty. Adding
your item onto a teammate's not-yet-consented list records nothing, so the
full list still reaches the gate. Trust pins to the exact list, so any edit —
a teammate's git pull included — re-arms the gate. Expect one prompt per
shared list per machine, and one after every change to it.
Concurrency expectations
Every wiki write (/s:teach, a queued question, an answer, a discard)
auto-commits locally under an exclusive lock. Concurrent writers land
their own commits, never racing git's index lock. A session-boundary hook
fetches, fast-forward merges, and pushes at session start, then pushes again
at session end. Git — not shipd — reconciles whatever stays unsynced:
- Per-page files merge cleanly. Concurrent edits to distinct
wiki/*.mdpages never conflict. queue.mdis a conflict surface. The engine appends new questions at EOF, so two parallel queue writes land at the same place. Keep both blocks when you resolve.index.mdis a conflict surface. Every wiki emission rewrites the catalog wholesale, so parallel page installs collide there. Keep every entry from both sides.- Duplicate
q-<slug>blocks leave the queue invalid. Slugs are unique acrossqueue.md. A merge keeping two blocks with one slug invalidates the store, and later queue writes fail until you remove or rename one. Check the merged file for repeated## q-headings before you commit. - Two answers to one question conflict on its
Answer:line. Both edits land on the same line of the same block. Keep exactly one answer, never a concatenation of the two: the block holds a singleAnswer:value. Pick the answer the team stands behind, and say so in the resolving commit.
store_autocommit turns off the local auto-commit; store_sync turns off
the session-boundary hook's fetch, merge, and push. Both default true, and
branching the workspace repo, or not, remains entirely your team's call.
An enterprise example: one workspaces repository
Take an enterprise with several engineering groups — discovery and
platform, each holding three or four teams. They share repos the whole
organization owns, with discipline codeowners approving every pull request.
One dedicated workspaces repository, itself a workspace, holding one
--nested team workspace per team or group. The repo root carries an
organization-wide manifest and wiki. Each team's own folder is its own nested
workspace, with its own manifest and wiki store.
Practical examples carries the layout's mechanics.
Build it with the guided wizard, run from inside the base:
git clone git@github.com:acme/workspaces.git ~/workspaces/acme
cd ~/workspaces/acme
shipd workspace team
Reads fall through, writes land nearest. A team folder that declares no
projects of its own inherits the base's; one that does shadows it instead.
Every wiki write — /s:teach, a queued question, an answer — lands in the
nearest store. That's a team's own when run from inside it, the base's only
when run from the base itself. A question queued at the base is answerable
only there.
Projects are systems, not teams. The manifest forbids two projects
claiming one repo path, and shared repos are exactly that: no team owns
api-core. Declare projects along the seams your codeowners already draw. The
main-app project takes the frontend and mobile repos, api the repos the
backend discipline owns, and infra the deployment and platform repos. Two
folders declaring the same api-core duplicate nothing: uniqueness holds per
manifest, and each is that team's own view. A team is instead who works an
initiative — a Project:-scoped brief every clone reads.
Nobody materializes everything. The sync plan is advisory per member. An engineer who works three of the declared repos runs the commands for those three and skips the rest. An absent member draws a note, never an error. An engineer who already has a checkout elsewhere maps it instead of re-cloning, with a member map beside the manifest. The map is machine-local and never committed, so one layout never leaks into another.
A group whose knowledge must stay isolated gets its own repository. Git has no per-directory permissions, so everyone who clones the workspaces repo reads every team folder in it. That includes the base wiki and every nested team's own store. Give that group a workspace repository of its own — separate repos, not directories, are the isolation boundary.
Day to day, shipd workspace prints a team's roster and shipd workspace sync
re-checks the members you use. shipd board covers every declared project's
repos from the workspace root, and only those beneath the folder it runs from.