Configuration & files
The gs CLI keeps two kinds of state on disk: your logins, which live in your
home directory and are shared across every repo, and per-repo bookkeeping, which
lives in a .gitstack/ directory inside each clone. One repo file is tracked
and travels with the repository: .gitstack/policy.toml, which sets what a
review needs before it lands. Who can read, write, and own a repo is not a
file; it is set on the repo's settings page (see
Repository access). This page documents each file, its
schema, and which commands write it.
~/.config/gs/config.toml#
Your default login. gs login writes it after a successful browser handshake,
and every command that talks to the server reads it (unless a named profile is
selected; see below). Absence means "logged out": commands that need auth print
not logged in; run gs login first.
server = "https://gitstack.sh"
token = "gsd_9f3c...redacted"
handle = "alice"
| Field | Type | Meaning |
|---|---|---|
server |
string | Normalized base URL of the GitStack endpoint this login talks to. Always https://gitstack.sh unless you logged in with gs login --server against a development build. |
token |
string | The device token minted during the handshake. Sent as a bearer token on every API call. |
handle |
string | The account handle this login belongs to. gs auth list shows it per profile. |
default_profile |
string, optional | Name of the profile that answers when nothing else selects one. Set by gs auth use <name>. Absent means the default login in this file answers. |
[update] |
table, optional | Cadence for the update nudge gs prints when the server advertises a newer release. |
serverhttps://gitstack.sh unless you logged in with gs login --server against a development build.tokenhandlegs auth list shows it per profile.default_profilegs auth use <name>. Absent means the default login in this file answers.[update]The file is written with 0600 permissions inside ~/.config/gs/ (created
0700). gs whoami prints a summary of the active login without exposing the
token.
gs logout revokes every device token you own on the server, then removes the
selected profile's config.toml and token file. Other profiles keep their
sessions. It is idempotent: running it while already logged out does nothing.
Auth profiles#
One machine can hold several signed-in identities, say a personal account and a work account on the same server, or accounts on two different servers. Each extra identity is a named profile:
gs login --profile work
That writes ~/.config/gs/profiles/work/ with its own config.toml (server,
token, handle) and token sidecar, in the same shapes described on this page.
The plain gs login identity is the default profile and stays where it always
was, directly in ~/.config/gs/.
Every command picks exactly one profile to act as, checking in order:
- The
--profile <name>flag on the command itself. - The
GS_PROFILEenvironment variable. - The repository pin, if the current repo has one.
- The global default:
default_profileinconfig.toml, else the default login itself.
A selector that names a profile that does not exist is a hard error. gs never falls through to a different identity than the one you asked for.
Two commands manage the roster:
gs auth list # every profile: server, handle, default marker,
# and which one answers in this directory
gs auth use work # set the global default
gs auth use work --repo # pin the current repository instead
The repo pin is a one-line file at .git/gs-profile (in the git common
directory, so every linked worktree of the checkout shares it). It is
machine-local and never committed: your clone acts as work without imposing
anything on anyone else who clones the repo.
The git credential helper#
gs login also sets up plain git push to authenticate against the same
server, so pushes from inside a stack do not prompt for a password. Alongside
config.toml it writes two more files into ~/.config/gs/:
| File | Mode | Purpose |
|---|---|---|
token |
0600 |
The device token as a bare string. The helper reads it. |
credential-helper.sh |
0700 |
A short shell script that answers git's credential query with username gs and the token as the password. When gs runs git on your behalf it exports GS_PROFILE, so the helper answers with the active profile's token; outside gs it uses the default profile. |
token0600credential-helper.sh0700git's credential query with username gs and the token as the password. When gs runs git on your behalf it exports GS_PROFILE, so the helper answers with the active profile's token; outside gs it uses the default profile.It then adds a global git config entry so git runs that helper for your server's URL only:
credential.https://gitstack.sh.helper = !/Users/you/.config/gs/credential-helper.shThe entry is reset before it is added, which clears any inherited helper (such as the macOS keychain helper Xcode ships) for that one URL. Everything else in your git config is left alone.
gs logout removes the selected profile's config.toml and token, which
is enough to stop authenticating as that identity. It does not delete
credential-helper.sh or the git config entry. Both stay but go inert once
no profile has a token left to read. A later gs login reuses them.
The .gitstack/ directory#
Every clone has a .gitstack/ directory at its root holding gs's per-repo
bookkeeping. gs creates it on first use and adds two lines to the repo's
machine-local ignore file, .git/info/exclude: /.gitstack/* and
!/.gitstack/*.toml. The bookkeeping never shows up in git status, never
lands in a commit, and is never imposed on anyone else who clones the repo.
The exclude lines are written once and shared across every worktree of the
clone.
The same directory holds the repo's committed config: policy.toml and
checks.toml. The *.toml exception keeps those visible,
so you change them like any other file: edit, gs add .gitstack/policy.toml,
commit, and land the review. gs add stages config files even in clones
that ignore the whole directory.
Everything in .gitstack/ other than the .toml config is gs-owned state.
gs reads and rewrites it between commands, and the manifest is hashed and
pushed verbatim, so a manual edit either gets clobbered or desynchronizes
your local view from the server.
The directory holds the manifest, a couple of navigation and autostack sidecars, and a set of short-lived files that carry state from one command to the next:
| File | Written by | Read by |
|---|---|---|
branch-reviews.json |
most review verbs | everything that needs the stack shape |
last-branch |
gs checkout when it detaches |
gs checkout on the way back |
autostack-fixes.json |
gs autostack |
inspection after a gated autostack run |
pending-force.json |
gs commit / gs amend after a non-fast-forward rewrite |
gs push (then cleared) |
pending-resolves.json |
commits carrying --resolves |
gs push (then cleared) |
push-rejected.json |
gs push on a rejected push |
reconciliation gates (then cleared) |
branch-reviews.jsonlast-branchgs checkout when it detachesgs checkout on the way backautostack-fixes.jsongs autostackpending-force.jsongs commit / gs amend after a non-fast-forward rewritegs push (then cleared)pending-resolves.json--resolvesgs push (then cleared)push-rejected.jsongs push on a rejected push.gitstack/branch-reviews.json#
The manifest. It maps the current branch to its tip commit and lists, bottom to top, the reviews carved out of the branch with each review's tip SHA. gs pushes this file to the server as a git blob, so the on-disk bytes and the pushed blob hash to the same SHA when they match.
{
"branch": "add-billing",
"branch_tip": "8f2a1c9e4b7d0a6f3c5e8b1d2f4a7c9e0b3d6f8a",
"reviews": [
{ "review_id": "R412", "tip_sha": "1a2b3c4d5e6f7081920a1b2c3d4e5f6071829304" },
{ "review_id": "R413", "tip_sha": "8f2a1c9e4b7d0a6f3c5e8b1d2f4a7c9e0b3d6f8a" }
],
"fixes": [188, 190]
}
| Field | Type | Meaning |
|---|---|---|
branch |
string | Branch this manifest describes. A manifest loaded on a different branch is refused as stale. |
branch_tip |
40-char SHA | Current tip of the branch. Must equal the last review's tip_sha. |
reviews |
array | Reviews in stack order, bottom first. Each row is a review_id (like R412) plus its tip_sha. |
fixes |
array of ints | Issue numbers this stack closes. The link is branch-scoped: the server closes each issue only once every review on the branch has landed or been abandoned, never when a single review lands. |
branchbranch_tiptip_sha.reviewsreview_id (like R412) plus its tip_sha.fixesA resolves field also exists on the wire to carry drained comment-thread ids
to the server on push, but it is stripped from the persistent on-disk form.
For how reviews and stacks map onto this file, see Stacks & navigation and Reviews.
.gitstack/last-branch#
A one-line file naming the branch you were on. gs checkout r5 detaches HEAD at
a review's tip so you can inspect it; before detaching, gs records your branch
here. Switching back to a real branch clears the file. That is how gs knows
which branch to return you to after you finish looking at a detached review.
.gitstack/autostack-fixes.json#
A sidecar written by gs autostack when its gate applies auto-fixes on top of
your reorganized work. It records what the gate flagged and the exact diff the
fixes introduced, kept separate from "your work, reorganized" so you can review
the machine's changes on their own.
{
"flags": [ /* findings the gate raised */ ],
"gate_delta": "diff --git a/... (unified diff of the auto-fixes)"
}
Repository access#
Who can read, write, and own a repo is set on the repo's settings page, under Access. It is not a file in the repository, so moving a branch never changes who has access.
| Role | Grants |
|---|---|
| owner | Full control: manage access, land anyone's review, administer the repo. |
| write | Push branches and land their own reviews. |
| read | Read-only access. |
The person who created the repo is always an owner and is not listed. In an organization, members reach every repo through their org role as well. Repo owners, org owners and admins can add someone by handle, change a role, or remove someone. A change takes effect right away and is recorded in the audit log.
Repos created with an older GitStack may still have a .gitstack/owners.toml.
GitStack no longer reads it, and the settings page points it out so you can
delete it.
.gitstack/policy.toml#
The repo's landing policy: which CI checks must pass and who must approve a
review before it lands, and what happens when a land turns the default branch
red. It also lists branches gs repo prune must keep. It is tracked, lives on the
default branch, and changes only through a review that a repo owner approves. The
policy that applies to a land is the one already on the default branch, never
the one in the review being landed.
# .gitstack/policy.toml
[checks]
enabled = true
required = ["build-and-test", "lint"]
[approvals]
min_lgtms = 2
require_requested_reviewers = true
owners_bypass = false
[[approvals.rules]]
paths = ["docs/**", "CHANGELOG.md"]
min_lgtms = 0
[[approvals.rules]]
paths = ["src/billing/**"]
min_lgtms = 3
owner_lgtm = true
[prune]
keep = ["staging", "release/*"]
[mirror]
github_managed = ["production"]
[landing]
on_red = "revert"
Every table is optional. Without the file, a review needs no LGTM, owners may land anything, and no checks are required. A change to an owner-controlled file still needs an owner's LGTM.
[approvals]:
| Key | Type | Default | Meaning |
|---|---|---|---|
min_lgtms |
integer, 0 to 10 | 0 |
Active LGTMs a review needs from writers or owners other than its author. 0 lets a writer land their own review with no LGTM. |
require_requested_reviewers |
bool | false |
Every requested reviewer who can approve (a writer or owner other than the author) must LGTM. Readers are skipped. |
owners_bypass |
bool | true |
Repo owners may land without meeting the rest of the policy. Set it to false and owners need the same approvals as everyone, automerge included. |
min_lgtms00 lets a writer land their own review with no LGTM.require_requested_reviewersfalseowners_bypasstruefalse and owners need the same approvals as everyone, automerge included.Site admins can always land, so a repo can't lock itself out. Automerge never gets the owner bypass.
[[approvals.rules]] set the approvals for the files they match, so review
effort goes where the risk is:
| Key | Type | Meaning |
|---|---|---|
paths |
array of globs, required | The files the rule covers. Repo-relative, no leading /. * stays within one directory; ** as a whole segment matches any depth, so docs/** covers everything under docs/. |
min_lgtms |
integer, 0 to 10, required | LGTMs a review changing a matched file needs, in place of the top-level min_lgtms. |
owner_lgtm |
bool, default false |
A review changing a matched file also needs a repo owner's LGTM. |
paths/. * stays within one directory; ** as a whole segment matches any depth, so docs/** covers everything under docs/.min_lgtmsmin_lgtms.owner_lgtmfalseEach file a review changes takes the last rule that matches it, as in
CODEOWNERS; files no rule matches take the top-level min_lgtms. The review
needs the most any of its files needs, counting only the review's own
changes, not the rest of its stack. gs stack, gs submit and the review
page name the rule that decided: needs 1 more LGTM (1 of 3, src/billing/**).
If GitStack can't list a review's files, it applies the strictest rule.
[checks]:
| Key | Type | Meaning |
|---|---|---|
enabled |
bool | Turn the required-checks gate on or off. On a repo that mirrors to GitHub it defaults to on once required names a check. |
required |
array of strings | Check names that must pass on the exact commit being landed. Names match the check runs GitHub reports. |
dispatch |
string | A GitHub Actions workflow file, like "ci.yml", that GitStack starts on a review branch when the required checks have not run on its tip: at land time (gs submit waits and lands once they pass), from automerge, or on request with gs checks run. The workflow needs a workflow_dispatch trigger, and the GitHub App needs the Actions read and write permission. Empty (the default) leaves CI to GitHub's own triggers. |
enabledrequired names a check.requireddispatch"ci.yml", that GitStack starts on a review branch when the required checks have not run on its tip: at land time (gs submit waits and lands once they pass), from automerge, or on request with gs checks run. The workflow needs a workflow_dispatch trigger, and the GitHub App needs the Actions read and write permission. Empty (the default) leaves CI to GitHub's own triggers.[prune]:
| Key | Type | Meaning |
|---|---|---|
keep |
array of globs | Branches gs repo prune never deletes, whoever runs it. * does not cross a /, so release/* keeps release/1.2 but not release/1/hot. |
keepgs repo prune never deletes, whoever runs it. * does not cross a /, so release/* keeps release/1.2 but not release/1/hot.[mirror]:
| Key | Type | Meaning |
|---|---|---|
github_managed |
array of globs | Branches GitHub owns, such as a deploy branch a script pushes to GitHub directly. GitStack never pushes them, always takes GitHub's version (rollbacks included), refuses pushes and lands into them, and never prunes or deletes them. Can't match the default branch. |
github_managed[landing]:
| Key | Type | Default | Meaning |
|---|---|---|---|
on_red |
"revert" or "notify" |
"revert" |
What GitStack does when a land turns a required check red on the branch it landed into. "revert" reverts the land that broke it and lands the revert; "notify" only comments on the reviews. See When a land breaks the branch. |
on_red"revert" or "notify""revert""revert" reverts the land that broke it and lands the revert; "notify" only comments on the reviews. See When a land breaks the branch.An invalid file counts as "notify".
The file is read strictly. A typo like min_lgtm, a wrong type, a
min_lgtms outside 0 to 10, a path rule missing paths or min_lgtms, a
malformed glob, an on_red other than "revert" or "notify", or a
github_managed glob that
matches the default branch makes it invalid, and an invalid policy refuses
every land except a repo owner's, so an owner can land the fix. It also
refuses every prune, since the branches it means to keep are unknown. The repo
settings page shows the policy in force, and flags an invalid file.
Repo owners can also change the policy from the repo settings page. Like a
collaborator change, saving does not touch the default branch: it opens (or
updates) a review on the reserved settings/policy branch that edits
policy.toml for you, and the new rules take effect when a repo owner lands
it.
Migrating from checks.toml#
Repos that set required checks before policy.toml existed have a
.gitstack/checks.toml with enabled and required at the top level. It
keeps working, and migrating is optional and manual:
- In a review, add a
[checks]table to.gitstack/policy.tomlwith the sameenabledandrequiredkeys, and have a repo owner approve and land it. - Once it lands,
policy.tomlwins andchecks.tomlis ignored. The repo settings page says so. Deletechecks.tomlin a later review.
A partial migration is safe. A policy.toml with only an [approvals] table,
or with an empty [checks] table, leaves checks.toml in force.