Access — identities, tokens, and remote permissions
The yggdrasil workspace runs with two distinct permission systems that are easy to confuse but answer different questions:
| Question | Mechanism | Doc |
|---|---|---|
| Can the agent run this shell command without a confirmation prompt? | .claude/settings.json allowlist + Claude Code matcher |
permissions.md |
| Can the agent perform this remote Git operation on a repo? | Token scope + collaborator/fork status + identity selection | this doc |
Both layers must approve an action that crosses both surfaces. A
git push is allowed locally (it's in the matcher's auto-allow set
or an explicit pattern), but the remote rejects it if the PAT lacks
push scope or the agent identity isn't a collaborator on the repo.
Layered defense.
This doc covers the remote layer: who the agent is, what tokens it holds, and how the workspace's fork-and-PR pattern keeps human and agent contributions cleanly attributed.
1. Two identities, one workspace
A GDD session can have two Git identities in play:
- The human contributor (you). Your GitHub/GitLab username, your PAT for any commands you run interactively. Used for committing via local git config.
- The agent identity (e.g.
agent-refr). A separate account with its own scoped PAT. Used forws push,ws cr,ws reviewreply/resolve, andgh apicalls that happen via the agent's tooling.
Why separate identities? Three reasons:
- Reviewable attribution. Every commit, PR, and review thread
is clearly authored by a human or an agent. Co-Authored-By
trailers in commit messages preserve the pairing; the actual
Author:field reflects whoever ran the operation. - Scope minimization. The agent PAT can hold just enough
scope for routine work (
repo, plus a couple of read scopes). The human PAT might hold more (workflow editing, package publishing, etc.). Compromise of the agent token is bounded. - Accountability and revocation. If something goes wrong, you can revoke the agent token immediately without affecting your own day-to-day Git access.
Setup mechanics — installing the CLI tools, generating the tokens,
loading them via .env — live in
docs/git-provider-setup.md. This doc
focuses on the patterns that compose those tokens with the
workspace's fork-and-PR model.
2. The forkOrg pattern
For repos owned by an org you're not a maintainer of (typical for upstream open-source contribution), the workflow is:
- The agent forks the repo under its own org (
forkOrg). ws push <component> <branch>pushes to that fork.ws cr <component> "<title>" <bodyfile>opens a PR from the fork to the upstream.
The forkOrg is configured per-developer in ecosystem.local.yaml
under identity.forkOrg:. With a single remote on a component, no
disambiguation is needed; with multiple remotes (e.g. you've added
both upstream and a personal fork manually), forkOrg picks which
side to push to.
For repos you DO own (your personal namespace), no fork is needed — the workspace just pushes to your remote directly.
3. The collaborator pattern (personal repos)
For personal repos — most commonly hoards (hoards/thalami-<user>/)
or personal components — the agent needs write access without forking.
The pattern: add the agent identity as a collaborator on the
GitHub repo.
Steps for a thalami hoard:
- Create the personal repo:
gh repo create <user>/thalami-<user> --private --source=hoards/thalami-<user> --remote=<user> --push. - Add the agent as collaborator:
gh api repos/<user>/thalami-<user>/collaborators/agent-refr \
-X PUT -f permission=push
- The agent's PAT (which has
reposcope) now inherits write access via the collaboration —ws push thalami-<user>works without needing the agent to fork your personal repo.
This pattern is key for GDD's "personal piles" story. Without it, multi-machine thalami sync would require manual git operations from the human's account on every machine.
4. Token scopes in practice
The agent PAT's scope set determines what it can do remotely. The recommended baseline is one write scope plus three read scopes — all the read scopes are low-risk additions that just remove recurring papercuts in routine PR/review operations:
| Scope | What it enables |
|---|---|
repo |
Full read/write on accessible repos. PRs, issues, releases, code, branches. The one write scope. |
read:org |
Read org membership. gh pr edit and several other org-aware operations need this. |
read:discussion |
Read discussion threads. gh pr edit checks this. |
read:project |
Read GitHub Projects (boards, items). gh pr edit --body-file checks this. |
workflow scope is needed only if the agent edits files under
.github/workflows/. Most GDD work doesn't touch CI definitions.
The three read:* scopes don't grant any mutation power and don't
unlock any code the agent can't already reach via repo — they're
metadata reads that gate routine gh pr edit-shaped operations.
Compromise of the agent token isn't meaningfully expanded by adding
them; declining them just means hitting the gh api PATCH fallback
below for every PR-edit-shaped call.
Strict-minimum (when org policy or paranoia gates the read
scopes): repo alone, with the gh api -X PATCH
repos/<owner>/<repo>/pulls/<n> --input <file> workaround for
PR-edit operations. gh api expects the HTTP method via
-X/--method, not as a positional argument. On Windows Git
Bash, drop the leading / from the API path to avoid MSYS path
conversion rewriting it as a Windows filesystem path.
When in doubt about whether a token covers an operation, run
ws diagnose <component> — it reports per-component remote
detection, expected token variable, and whether the token is
present and authenticated.
5. Multi-provider workflows
The workspace is provider-agnostic. A component on GitHub uses
GH_TOKEN; a component on GitLab uses GITLAB_TOKEN; a self-hosted
Forgejo or Gitea instance uses whatever variable you map under
defaults.gitTokens in the realm or local config.
ws push, ws cr, ws review, etc. all auto-detect the provider
from the component's remote URL and pick the right CLI (gh vs
glab) and the right token. Mixed-provider workspaces (some
components on GitHub, some on GitLab) work without per-command
configuration — the dispatcher handles it.
Tokens for additional providers go in .env:
export GH_TOKEN=ghp_xxx
export GITLAB_TOKEN=glpat_xxx
# Add more as needed for self-hosted instances
The mapping from provider → token variable lives in your realm's
ecosystem.yaml under defaults.gitTokens, or override per-developer
in ecosystem.local.yaml.
Default behavior with no gitTokens entry. For GitHub remotes,
gh reads $GH_TOKEN automatically — no gitTokens entry needed
in the common case. Same for GitLab: glab reads $GITLAB_TOKEN.
ws diagnose reports this as ✓ <VAR> is set (default for
<provider>; no gitTokens entry needed). You only need an explicit
gitTokens entry when you want fine-grained per-namespace control
— e.g., the org repos use the team's GH_TOKEN but your personal
forks under <your-user> should use a different GH_TOKEN_PERSONAL.
In that case, add a longer-prefix gitTokens entry that wins the
longest-prefix match for the personal namespace:
defaults:
gitTokens:
github.com: # default for any GitHub remote
var: GH_TOKEN
github.com/<your-user>: # personal forks override
var: GH_TOKEN_PERSONAL
Without that entry, both namespaces flow through the same default
GH_TOKEN — fine for most setups; only worth splitting when the
isolation is intentional.
GitLab specifics (gitlab.com or self-hosted)
GitLab's API model differs from GitHub's enough that the same abstract pattern (fork → push → MR) plays out differently in practice. Notes from real workflows on self-hosted GitLab:
The biggest model flip: on GitHub, gh pr create POSTs to the
upstream's API. On GitLab, glab mr create POSTs to the
fork's API — the --repo flag looks like a target but the
actual POST goes through the fork project. Consequence: the fork
write token, not the upstream reporter token, is the one needed
for MR creation, even though the MR targets upstream. This may catch
people the first time.
Two-token model. Every fork-based GitLab operation needs two
distinct tokens, configured in defaults.gitTokens (longest URL
prefix wins, so a fork-group entry shadows the upstream group entry
for repos in the fork namespace):
| Token | Role | Used for |
|---|---|---|
| Fork write | Developer on fork group | Push branches, create MRs |
| Upstream reporter | Reporter on upstream group | Read default branch, read MR threads (ws review), file issues |
GitLab PATs cannot be downscoped. A Personal Access Token runs
as you and inherits your full account access — there's no way to
issue a "read-only on group X" PAT when you're an Owner there. The
GitLab-native workaround is Group Access Tokens or Project
Access Tokens, which carry an explicit role independent of any
user. On paid tiers and self-hosted instances, create one at
Settings → Access Tokens for the group/project. On gitlab.com
free tier these aren't available — point both token vars at the
same PAT and accept the over-scope.
Bot attribution trade-off. MRs opened via a Group Access Token
appear as authored by project_NNN_bot_..., not your personal
account. The workspace mitigates this with two layers:
(1) the fork namespace path makes your username visible in the MR
source header, (2) @HUMAN_ACCOUNT substitution puts your handle
in the MR body. Adequate for team workflows; not sufficient for
formal audit trails that require the GitLab author field to be a
human identity.
glab gotchas worth knowing up front:
glab issue listand several other subcommands requireGITLAB_HOSTin the environment for self-hosted instances. There's no--hostnameflag on subcommands. Set it in.env.glab auth loginconfigures SSH for API calls but does not store HTTPS git credentials.git clone/pushover HTTPS will hang waiting for a password. Either use SSH URLs, or rungit credential approveto cache the token in your keychain. Thews gitlab-authsubcommand registers the token with glab and caches HTTPS credentials in one step.GITLAB_TOKENmust be set forglabto authenticate (parallel toGH_TOKENforgh).glab mr create --head <ref>expects the full fork project slug (e.g.<your-user>/<repo>), not just a branch name.
identity.forkOrg must match the GitLab namespace exactly. If
your forks live at gitlab.com/<your-user>/..., set forkOrg:
<your-user> in ecosystem.local.yaml. Easy to get wrong if your
fork namespace differs from your login handle.
The verified workflow on GitLab (in practice on self-hosted, mirrors the GitHub flow but with provider-specific details substituted):
ws diagnose <comp> # check token coverage first
git checkout -b type/description # topic branch
# edit
ws commit <comp> <bodyfile>
ws push <comp>
ws cr <comp> --upstream "title" <crfile> # opens MR fork → upstream
ws review <comp> <mr#> # fetch CR threads
# fix → ws push → repeat until approved
# merge via GitLab UI
git checkout main
git pull <upstream-remote> main
Running ws diagnose <comp> before the first push to a new
component reveals missing token vars before you hit a 403 mid-CR.
6. Diagnostics: ws diagnose
When an operation fails for what looks like an access reason, run:
ws diagnose <component>
It reports:
- The detected remote URL and provider.
- The expected token variable for that provider.
- Whether the token is set in the environment (✓ / ✗).
- Whether
gh auth status(or equivalent) succeeds. - Whether the agent identity has access to the remote (read at minimum; push requires further verification by attempting one).
If ws diagnose shows ✗ for token coverage, the operation will
fail with an opaque auth error at runtime; fix the missing token
or scope first. The orientation skill runs ws diagnose proactively
on components likely to be pushed during the session — see
gdd-orientation Step 6b.
7. Future direction: scope-templated PATs
Today's pattern asks the human to manually create a PAT with the
right scopes. A future improvement is workflow-aware scope templates
— "workflow A needs scopes X+Y; workflow B needs X+Z" — surfaced
as ws diagnose recommendations or ws auth setup interactive
flows. Not implemented in v1; tracked under the broader
"onboarding and identity" design doc (see Thalamus / project-level
backlog).
See also
- Permissions — the local permission system (shell command allowlisting). Pairs with this doc; together they cover "what the agent can do."
docs/git-provider-setup.md— setup mechanics: installing CLIs, generating tokens, loading via.env.- Trust and Safety — the broader trust hierarchy that places agent vs human identity in context.
- Hoards — the canonical case for the collaborator pattern (personal multi-machine sync).
- Features Tour — where access control fits in the larger feature set.