Work with jj
Iron rules
- Raw Git is read-only in a colocated repository; every VCS mutation goes through jj, non-interactively.
- Run
jj statusafter each jj mutation. - By session end every change is landed (
jj land) or parked (jj park <name>); a floating unlanded change is a defect, not a state. Landing your own finished stack on local trunk is the default and needs no authorization; integrating other writers' work, pushing, releasing, or deploying still requires explicit authorization. - A conflicted bookmark (
main??, "Namemainis conflicted") makes that name an ambiguous revset: stop, reconcile it, and say so — never describe the graph as if the name resolved. How, and why the Git side looks fine: parallel-agents.md. Same for a(divergent)change, even thoughmainstill resolves. - After
jj land, read its hanging-heads report: integrate, park, or report each item — never ignore it. - If shared
@holds a change you did not author, open your own change before editing files. - Never publish a tip that does not contain the local trunk bookmark: landed-but-unpublished
work exists exactly there, and pushing around it forks the shared trunk (a conflicted
bookmark for every other writer). Before
jj git push, verify withjj log -r 'main & ~::<push-tip>'— empty output or stop. - Land trunk with
jj land [<rev>], never rawjj bookmark set main: it enforces forward-only moves (a sideways move orphans the old line) and refuses conflicted stacks. If it refuses, follow the recovery it prints and accept rebase conflicts — conflicts are recoverable, orphaned lines get lost.jj orphanslists unreachable non-empty heads; run it when finishing multi-writer work. jj abandonis the back door around every trunk guard: jj deletes bookmarks pointing at an abandoned commit and rebases its descendants, so one command removes trunk and rewrites landed history while reporting it like any success. Abandon only your own unlanded change, and give it a full commit id you resolved and read in the same breath — never@,@-or a revision captured earlier, because a revision resolves when the command runs and a shared@moves between your commands.- Where more than one workspace exists, never rewind operations —
jj undo,jj redoandjj op restoreall restore the repository-wide view and silently revert concurrent writers (measured:undoleft the caller's own mistake in place and reverted a co-worker's change;op restorefixed the caller and still reverted the co-worker). Recover forward with change-scoped commands instead —jj describe,jj abandon,jj restore --from, orjj new <lost-commit>— which leave other workspaces untouched. For the same reason--at-opis inspection-only; mutating under it forks the operation log and can drop bookmarks and commits from the visible graph. - A revset that matches nothing returns empty, not an error, so a wrong query reads as "no such
commit"; judge ancestry with a predicate (
jj log -r 'A::B'), never by eyeballing a listing. Both traps and their exact forms: parallel-agents.md.
Land, park, see (host-provided commands)
These wrappers exist on this machine; prefer them over raw bookmark commands.
jj land [<rev>] [--to <bookmark>]— move trunk to<rev>(default@-). Refuses: a conflicted trunk, a sideways move, a target missing published@originwork, conflicted or divergent commits, a<rev>that is not exactly one commit, a path inland.forbidden. On refusal it prints what to run. After any land it lists the files the range added (read everyWARN) and heads hanging outside every bookmark: integrate, park, or report each.jj park <name> [<rev>]— setparked/<name>. Positional arguments only, no flags. Default revision:@when it is non-empty or described, else@-. Verify the printed commit id is the change you meant.jj orphans— non-empty heads unreachable from every bookmark; run when finishing multi-writer work.
Establish the write root
Run jj root from the intended repository. If it fails, do not apply this skill unless the task
is explicitly migrating the repository to jj.
Stay in the checkout or workspace where the task started or that the launcher assigned. Never
switch to another checkout of the same repository while writing. Do not create branches, Git
worktrees, or jj workspaces unless the user explicitly asks or a launcher assigns one. Any
jj workspace add needs an explicit -r <resolved commit id>: without -r it starts at the
parents of the creator's @, and with an ambiguous or conflicted name it registers the workspace
anyway on an unintended base. Verify the new workspace's @- before working in it.
In a colocated repository Git's HEAD is normally detached and tracks @-, not @, so every
Git-side view is one commit behind your working copy and tooling that asks for the current
branch (git branch --show-current, deploy or publish scripts) sees none and may refuse to run.
Do not "fix" this with git checkout; run branch-dependent tooling from a scratch clone.
Load any repository-specific jj companion skill before proceeding, and maintaining-vcs-hygiene
for the culture layer (litter, parking, handoff claims).
Work non-interactively
jj has no read-only commands: status, log, diff and even op log first snapshot the working
copy, absorbing whatever the filesystem holds (test output, coverage, editor backups) into the
current change. Inspect with jj --ignore-working-copy <cmd> (add --at-op=@ for the exact
current operation), and never run a bare jj command inside a workspace you do not own.
Use jj for every VCS mutation:
jj describe -m "<intent>"
# edit and test
jj status
jj diff --git
jj new -m "<next logical unit>"
- Always pass messages explicitly; never open an editor, pager, TUI, or diff editor. Every
history command has a non-interactive form:
describe -m,commit -m,squash -morsquash --use-destination-message,split -mwith filesets. If an editor opens anyway and fails, the message text survives at the*.jjdescriptionpath in the error; retry with the flag, do not retype from memory. - Keep one logical change per jj change.
The description carries what the code cannot
In jj the description is a living field, not a post-hoc label: state intent at jj new -m and
refine it with jj describe -m as understanding improves. What belongs in the first line and the
body is maintaining-vcs-hygiene §3. The jj-specific rule is that a placeholder ("next") is
acceptable only while the change is still empty — describe it before its first file edit lands,
because undescribed work is invisible intent.
To find the decision behind a line, use jj file annotate <path>, then jj show <change-id>
for the description body. Line-range history needs colocated read-only git log -L (legal).
A stray file is committed before you notice
By default jj has no untracked state: every new file that is not ignored joins the current change the moment
any jj command runs. Ignore rules therefore have to exist before work starts, and
.gitignore is not retroactive — adding an already-tracked path to it does not untrack the file,
and its later edits keep being snapshotted. Untrack with jj file untrack <path> (the file stays
on disk).
Treat a captured secret as an exposed credential, not a tidy-up: after untracking, the value is
still readable from that change's evolution history (jj evolog), so say so plainly and have the
credential rotated. Never report the deletion as a fix. Details:
secrets-and-litter.md.
Adding a file larger than snapshot.max-new-file-size (default 1 MiB, e.g. vendored or
generated sources) makes every jj command fail until resolved — and while snapshotting fails,
new work is not being recorded, so the safety net is off. Follow the one-shot hint from the
error, or raise the limit deliberately with jj config set --repo snapshot.max-new-file-size.
Recover and hand off
Inspect before recovery:
jj status
jj diff --git
jj op log
jj log -r 'all() & ~::<trunk>' # work no trunk reaches, including commits no bookmark names
Recover forward. Read jj op log and jj evolog to find the state you lost, then bring it
forward with change-scoped commands (jj new <lost-commit>, jj restore --from <rev>,
jj describe, jj abandon). Rewinding operations is a whole-repository act: reserve
jj op restore <operation> for a single-workspace repository, or when you have confirmed no other
writer has operated since that operation — jj's operation log does not record which workspace made
an entry, so in a shared repository you usually cannot establish that. Never jj undo.
If a workspace is stale, another writer rewrote its working-copy commit: inspect jj op log
to learn what happened before running jj workspace update-stale, because the update can
materialise a recovery commit from disk and bury the original incident. Conflicts may live in
commits, but do not integrate or push a conflicted stack.
At handoff, report the write root, checks run, conflicts, and remaining work, and identify the work three ways: change ID (survives rewrites — the logical identity), full commit ID (what the reader must actually find; squash and rebase replace it), and the operation ID the claim was read at. A report carrying only change IDs sends the next agent looking for a commit that no longer exists under that hash. Follow the repository-specific companion for integration, workspace cleanup, or push procedures.
Load only the relevant reference:
- More than one writer in the repository, or work that disappeared: parallel-agents.md
- A stray file, litter, or a captured secret in a change: secrets-and-litter.md
- Oversized or mixed changes: history-cleanup.md
- Authorized bookmark or remote work: publishing.md
- Wiring a new machine or repository (what goes in CLAUDE.md/AGENTS.md vs config vs skills): onboarding.md