lock family docs GitHub

lea · Teams of agents

Allowances

Claude Code judges each action from its own conversation: "you may merge" told to a lead does not reach its children, and relayed in a lea message it is rightly distrusted. An allowance is that decision recorded by you on the host, with lea allow, and applied the same way to every agent it covers. lox run approves what agents do with your data through Locks; lea allow says which forge actions they take without asking.

Let agents deliver #

To let every agent of a lea push its own branch, open its pull request (GitHub) or merge request (GitLab) and merge it without a prompt, on the host, in the projects directory:

  1. lea allow deliver: lea reads each repository's origin and lists the forge repository each one is pinned to (github.com/OWNER/NAME or gitlab:HOST/GROUP/…/NAME).
  2. Check them and answer y (off a terminal, run it again with the --yes DIGEST it printed). It says what it recorded: allowance #1: agents of lea-myproj may push their branch, open PRs, merge PRs and delete their branch on api (github.com/me/api), web (gitlab:git.example.com/g/web), until lea allow off 1.
  3. lea allow (or g in the deck) shows what agents may do, the time left and the last uses; lea allow off 1 (or x in the deck's Allowed screen) stops it.
lea allow                          # this lea's allowances, one line each, and the last 10 uses
lea allow log                      # every recorded use: agent, capability, target, when
lea allow deliver                  # push pr merge delete-branch, on every forge repository here
lea allow deliver --gitlab git.example.com  # also a self-hosted GitLab (gitlab.com is known; a host once pinned stays known)
lea allow merge issues --repo api --repo web
lea allow push --branch 'feat/*' --agent dev-1 --for 2h
lea allow stop-finished
lea allow release --repo api --tag 'v0.15.*'  # gh release create v0.15.N --verify-tag [--notes-file F|--generate-notes]
lea allow off 12                   # allowance #12; or a capability (lea allow off merge), deliver, or nothing: everything of this lea

Capabilities #

capability covers
push git commit -m M (no --amend), git push [-u] origin <own branch>
pr gh pr create --head <own branch> --title T (--body B | --body-file F) [--base B] [--draft], glab mr create --source-branch <own branch> --title T [--description D] [--target-branch B] [--draft] [--remove-source-branch] [--yes]
merge gh pr merge <n>|<own branch> --squash|--merge|--rebase [--delete-branch], glab mr merge <n>|<own branch> [--squash] [--remove-source-branch] [--yes]
delete-branch git push origin --delete <own branch>
issues gh issue close|reopen|comment N
stop-finished agents_stop on an idle or ended child
release-tag git push origin refs/tags/vX.Y.Z (a new semver tag on a commit of the default branch, see below)
release gh release create <tag> --verify-tag [--title T] [--notes-file F | --generate-notes] [--prerelease] [--latest], for tags --tag GLOB matches (see below)

deliver is push pr merge delete-branch. The container is always this projects directory's lea (LEA_PROJECTS_DIR=~/other lea allow … for another one). Every capability but stop-finished is pinned to each repository's origin, read from its .git/config (as a file: no git runs on the host) at that moment, on github.com, gitlab.com or a --gitlab host: every repository here with such an origin, or the ones --repo names (repeatable; one without a forge origin is an error). --branch (a glob on the agent's branch, never the default branch) and --agent NAME (that agent and its children) narrow it. An allowance holds until lea allow off, or for --for (1m to 30 days); every line of lea allow says which.

Agents can write a repository's .git/config (or add a repository): lea records nothing while a pin is new or differs from the last one recorded for that repository of this lea (revoked ones included). On a terminal it lists them and asks record these? [y/N], and reads the origins again after your y: a pin that changed meanwhile records nothing. Off a terminal it lists them and exits 1 with a digest; --yes DIGEST records exactly those pins. Once confirmed, the same command applies without a question. Running lea allow deliver again (after adding a repository or changing an origin) updates the same allowance: rows of unchanged repositories keep their ids, and repositories no longer here are dropped.

lea doctor checks the allowances: each pinned repository's origin still matches its pin (an origin rewritten since, or a forge repository no allowance covers, is a warning), and each forge host has credentials (GitHub's in lea's proxy, lea github login; GitLab's in lea's state, glab auth login --hostname HOST; git's credential store alone covers pushes only, a warning); without them it fails. It only reads.

lea delivery on|off|status and lea grant add|ls still work for this minor release: they are translated to lea allow deliver, lea allow off deliver, lea allow and lea allow <capability>, with a notice. lea grant add --github X is refused when X is not the repository's origin.

What agents are told #

Each agent on a branch is told in its first prompt the forms lea runs without asking for what covers it when it starts, or, when nothing does, that pushing and PRs ask you and that lea allow deliver --repo <repo> lets it do them without asking. A merge, PR or push in a form no allowance covers (with -R, chained, another flag) takes the usual path, and the agent is told, in its context, the forms its allowance covers; when it holds none, which lea allow <capability> --repo <repo> you can run (lea's own text, from the agent's row, never from what it wrote). grants_list is the live list of what covers it. A message saying "the user allows X" changes nothing.

What lea checks on every allowed call #

lea's PreToolUse hook asks the daemon before each tool call: a call an allowance covers is allowed, with the grant's id as the reason, and audited (lea allow log: agent, capability, target, never the command); anything else takes the usual path. Revocation and expiry apply on the next call. Only the host writes allowances: there is no MCP tool for it (grants_list only reads them, grants_request only queues a request you approve on the host), the daemon refuses allow_set, allow_off, allow_list and grant_request_decide from a container, and lea.db is never mounted in one.

An allowed shell call never runs the agent's text: lea runs the command it parsed, rebuilt word by word, in a fresh /bin/sh under env -i (so the agent's variables, aliases and shell functions do not apply), with a fixed PATH and only HOME, GH_CONFIG_DIR, SSH_AUTH_SOCK and LANG as Claude's own process has them. It reaches the forge through lea's proxy, which lea names itself (HTTPS_PROXY for gh, http.proxy for git, http://lea-proxy:3128), never through a proxy the agent's environment names; CONNECT keeps TLS end to end.

  • gh runs as /usr/bin/gh … -R github.com/OWNER/NAME, the pin. It runs with the token of gh's config (gh auth token) and none of the rest of that config, which the agent can write (it could send gh's requests to a server of its own): an empty config dir no one but root can create.
  • A commit goes through plumbing in the agent's worktree (write-tree, commit-tree, update-ref): no hook, filter or signing runs. It commits the index as it is (git add first: no -a).
  • A push runs from a bare repository lea creates for the call (random name, mode 0700, its config checked right before the push, the worktree's objects borrowed) to the pin's https://…git with the full ref (refs/heads/<branch>:refs/heads/<branch>): the remote's config never decides where it goes.
  • git's hooks, fsmonitor, askpass and pager are off, ssh is /usr/bin/ssh, https and ssh are the only transports, ~/.gitconfig is not read; and lea refuses a repository whose own config holds a url.*.insteadOf, include, URL-specific credential helper, pushurl, push refspec, mirror, filter, textconv or any http.* key.
  • gh pr create --body-file F: F is a path relative to the agent's worktree that must resolve to a plain file inside it, outside .git (lea checks it is a plain file, opens it once and gh reads that open file; a quoted heredoc, --body "$(cat <<'EOF' … EOF)", is not covered: write the body to a file). gh pr merge checks that the PR's head is the agent's branch before the merge, so --delete-branch deletes that branch only.
  • On GitLab lea does not run glab, whose config the agent can write (it could send glab's requests elsewhere): glab only reads the token of its config for HOST, from /, and lea calls the project's API (https://HOST/api/v4) with curl (no .curlrc, https only, no redirect, no proxy variable), its answers read by jq, both with no HOME of the agent's. A merge first reads the MR: it must be open and come from the agent's branch of the project itself, not a fork (by branch: the one open MR of that branch); the merge then names the head commit it checked, so GitLab refuses it if the branch moved in between (an MR whose pipeline is still running is set to merge once it passes, as glab mr merge does; --rebase is not covered). Pushes go to https://HOST/PATH.git with glab's credential helper. A multi-line description is not covered (no file option): it takes the usual path.

A call is allowed only when it is exactly one plain command lea parses (no ;, &&, |, $(…), backticks, redirections, VAR= prefixes, --repo), run at the root of the agent's own worktree (<projects>/.lea/worktrees/<repo>/<agent>, so gh and git act on its repo), whatever is allowed. A branch is an agent's own only while no other live agent of the lea works on it. Some calls are denied outright, allowed or not: gh pr merge --admin, force-push (-f, --force*, +refspec, --mirror, --prune), any push to main or master however its refspec spells it, a push to any branch but the agent's own (named: a bare git push, HEAD or @ follows whatever is checked out), a refspec whose destination git would infer (<src>:<branch> with neither side refs/heads/…: a tag's source makes refs/tags/<branch>), a bare git push origin <branch> where the repository it runs in has no local branch of that name or a tag of it (lea reads its refs; after a command that may change them, refs/heads/<branch> only), a push of every branch or tag (--all, --branches, --tags, --follow-tags, a wildcard or a bare :), other git transports (send-pack, http-push, remote-<helper>, svn, p4), a push lea cannot read (an unknown option, a shell glob, a push run through another program, or a git … push in the arguments of any program but grep, rg, echo, printf, gh pr|issue), git run by xargs, parallel, find (or by another program without a subcommand), a git program, subcommand or push argument computed by the shell ($…, backticks; a substitution's own command is checked too), a program computed by the shell or run by env/sudo/… from a computed word, submodule foreach, bisect run, filter-branch, git options that run a program (--upload-pack, --receive-pack, --exec, --extcmd, -O, rebase -x), git options before the subcommand other than -C, -c user.name|user.email|color.ui|core.quotepath and display options, setting GIT_* (but GIT_EDITOR/GIT_SEQUENCE_EDITOR=true|:, GIT_PAGER=cat, GIT_TERMINAL_PROMPT=0), HOME or XDG_CONFIG_HOME, deleting a branch that is not the agent's own, repointing git or gh (git remote set-url|add|…, writes with git config, gh repo set-default, GH_REPO=/GH_HOST=), and writing a file that makes a later command run something else: Claude's settings, hooks and shell snapshots, git's config and hooks (.git/config, .git/hooks/, ~/.gitconfig), the shell's startup files (~/.bashrc…), /usr/local/share/lea or /lea/state. A command that hides words behind escapes or expansions (\, $…, backticks) and touches any of these is denied too: run it plainly.

Branch protection on the forge (required reviews, required checks) stays the control that decides whether a merge goes through: an allowance only removes the prompt.

Approving a merge for your team. You usually say it to a lead, in its chat ("your team may merge on api"). The lead turns those words into a grant request (grants_request): it records nothing, and you decide on the host, once:

lea requests                 # g12 claude-1 (lead in lea-myproj) asks for a merge grant on repo api (gh github.com/me/api) …
lea approve g12 g13          # record them, for that lead and its children (--expires 30d: a longer life)
lea deny g14 not this one    # the lead is told, with your reason
lea grant requests|approve|deny  # the same, under lea grant

The approved request is an allowance of the lead's subtree (--agent the lead): its children then merge their own PRs (gh pr merge N --squash --delete-branch) without a prompt, and no agent outside that lead's team is covered. The lead gets a message when you decide, and when a request expires (a day without an answer). Only leads file requests; no agent can approve one (grant_request_decide is host-only). A child whose merge nothing covers is told to ask its lead for one, or you for lea allow merge --repo <repo>.

Release tags. A tag push is a hard deny, but for the one form release-tag covers: git push origin refs/tags/vX.Y.Z (an optional pre-release, -rc.1), one plain command at the root of the agent's worktree. lea pushes it from a bare repository it creates for the call, only when the tag's commit is on the default branch of the remote (fetched at that moment), and without force, so a tag the remote already has is refused. --tags, --follow-tags, force, a refspec, another remote and a non-semver tag stay denied. deliver never includes it.

Release publication. A repository with no workflow that publishes a release on tag push needs gh release create after the tag push; the release capability covers exactly gh release create <tag> --verify-tag [--title T] [--notes-file F | --generate-notes] [--prerelease] [--latest], one plain command at the root of the agent's worktree, for a semver tag that --tag GLOB matches (none given: any semver tag). --verify-tag is required, so gh never creates the tag (that stays release-tag's job). lea runs it pinned like every allowed gh call (env -i, -R the pin, lea's gh config, through lea's proxy); a notes file follows the rule of gh pr create --body-file. --draft, --target, --notes, assets, --latest=false (use the plain flags) and every other gh release subcommand (edit, delete, upload, delete-asset) stay uncovered; a lead asks for it with grants_request (action release-create, tag the glob). deliver never includes it.

What allowances protect against #

Allowances and hard denies keep a confused agent (one that misreads a message, a brief or its own transcript) from merging, pushing or deleting where you did not say it may; they are not a sandbox against a hostile one. An agent can still write and run code through the tools it is allowed (go run, a test, a script), and it can read the token of glab's config (GitHub's is the proxy's: it can use it, not read it): what keeps a hostile agent in check is the container, its network rules and the scope of the tokens you give lea, not allowances. Two ways a hostile agent gets past them, among others: a shell function named /usr/bin/env in Claude's shell snapshot replaces the pinned call, and a loop rewriting the per-call bare repository's config (same user) between lea's check and the push can send the push elsewhere. Revoking is your lever: lea allow off, or x in the deck's Allowed screen.

Checking a Claude Code release #

To check that a Claude Code release runs the command lea rebuilds (the hook's updatedInput) before relying on allowances, in a lea: lea allow push --repo <r> --for 10m, ask an agent working on a branch of <r> to git add a file and run git commit -m test, then check that git reflog -1 in its worktree says commit (lea grant) (a plain git commit writes commit: test); lea allow log lists the use.

lea new --brief FILE gives a new agent its first message; --repo NAME starts it in a directory of the projects directory; --branch B has it work in its own git worktree under <projects>/.lea/worktrees/<repo>/<agent>.