# Understanding the gh-stack Stack File Format and .git/gh-stack Management

> Learn about the gh-stack stack file format and how .git/gh-stack manages stacked branch metadata using JSON files with atomic operations and file locking.

- Repository: [GitHub/gh-stack](https://github.com/github/gh-stack)
- Tags: deep-dive
- Published: 2026-08-02

---

**The gh-stack CLI stores stacked branch metadata in a JSON file at `.git/gh-stack`, governed by a strict schema defined in [`internal/stack/schema.json`](https://github.com/github/gh-stack/blob/main/internal/stack/schema.json) and managed through atomic load/save operations with file locking in [`internal/stack/stack.go`](https://github.com/github/gh-stack/blob/main/internal/stack/stack.go).**

The `github/gh-stack` extension tracks stacked pull requests using a structured metadata file. This **gh-stack stack file format** persists branch relationships, commit SHAs, and pull request states between CLI invocations. The file lives in the repository's `.git` directory and follows a versioned JSON schema to ensure compatibility across different gh-stack versions.

## Core Structure of the Stack File

The `.git/gh-stack` file conforms to the JSON Schema defined in [`internal/stack/schema.json`](https://github.com/github/gh-stack/blob/main/internal/stack/schema.json). The Go implementation in [`internal/stack/stack.go`](https://github.com/github/gh-stack/blob/main/internal/stack/stack.go) unmarshals this data into strict types that enforce the schema.

### Root Level Fields

| Field | Go Type | Description |
|-------|---------|-------------|
| `schemaVersion` | `int` | Must be `1`. Future versions cause older binaries to abort with a version mismatch error. |
| `repository` | `string` | Fully-qualified repository name in `owner/repo` format. |
| `stacks` | `[]Stack` | Array of stack objects representing GitHub Stacks (groups of PRs). |

### Stack Object Structure

Each entry in the `stacks` array contains the following fields:

| Field | Go Type | Description |
|-------|---------|-------------|
| `id` | `string` | Global GitHub-wide stack identifier (opaque string). |
| `number` | `int` | Repository-scoped stack number shown in the UI; may be `0` in older files and is back-filled from the API. |
| `url` | `string` | Direct link to the Stack on GitHub (omitempty). |
| `merged` | `bool` | `true` when the entire stack has been merged (omitempty). |
| `trunk` | `BranchRef` | The base branch of the stack (usually `main` or `master`). |
| `branches` | `[]BranchRef` | Ordered list of branches from bottom (trunk) to top. |

### Branch Reference Fields

The `BranchRef` objects stored in `branches` and `trunk` contain:

| Field | Go Type | Description |
|-------|---------|-------------|
| `branch` | `string` | The branch name (e.g., `feat/login`). |
| `head` | `string` | SHA of the branch tip (omitempty). |
| `base` | `string` | SHA of the parent commit (omitempty). |
| `pullRequest` | `*PullRequestRef` | PR metadata including `id`, `number`, and `url` (omitempty). |
| `merged` | `bool` | `true` if the specific PR has been merged (omitempty). |
| `queued` | `bool` | Transient flag set when the PR is in a merge queue; **not persisted** (marked with `json:"-"` in the Go struct). |

## How the Stack File Is Managed in .git/gh-stack

The [`internal/stack/stack.go`](https://github.com/github/gh-stack/blob/main/internal/stack/stack.go) file implements atomic operations to prevent data corruption during concurrent access. According to the [`AGENTS.md`](https://github.com/github/gh-stack/blob/main/AGENTS.md) documentation, these primitives ensure safe manipulation of the JSON metadata.

### Loading and Initialization

When commands need stack data, the `Load(dir)` function reads `.git/gh-stack`. If the file is missing, it returns a new empty `StackFile` struct with `SchemaVersion` set to the current constant. The loader validates that `schemaVersion` matches the expected value, rejecting files written by newer tool versions with an explicit error message.

### Atomic Saving and Schema Versioning

After mutating the in-memory struct (e.g., adding branches via [`cmd/add.go`](https://github.com/github/gh-stack/blob/main/cmd/add.go)), the `Save(sf)` function writes changes back to disk. The implementation uses `json.MarshalIndent` for human-readable JSON and refreshes the `SchemaVersion` field to the current constant before writing.

### File Locking and Concurrency Control

To prevent race conditions, gh-stack acquires an exclusive lock on `.git/gh-stack.lock` before any write operation. The lock implementation enforces a 5-second timeout; if another process holds the lock, the operation fails with a `LockError`. This ensures that concurrent `gh stack` commands do not corrupt the JSON structure.

### Staleness Detection

The `Load` function calculates a checksum of the file contents when reading. On `Save`, the implementation compares the current on-disk checksum against the stored value. If they differ, indicating an external modification since the last load, the save aborts with a `StaleError`. This forces the caller to reload and re-apply changes, preventing silent overwrites.

## Practical CLI Usage

While the Go code handles the JSON directly, users interact with the file through CLI commands that internally call these safe operations:

```bash

# Initialize a new stack file (creates .git/gh-stack if missing)

gh stack init

# Add a branch to the current stack (updates JSON via Load/Save)

gh stack add feat/login

# Inspect the raw JSON for debugging

cat .git/gh-stack | jq .

```

The [`cmd/init.go`](https://github.com/github/gh-stack/blob/main/cmd/init.go) and [`cmd/add.go`](https://github.com/github/gh-stack/blob/main/cmd/add.go) entry points demonstrate how high-level commands delegate to the `Load` and `Save` functions, ensuring the locking and validation logic always applies.

## Summary

- The **gh-stack stack file format** is a JSON document stored at `.git/gh-stack` that tracks stacked PR metadata, branch SHAs, and merge status according to [`internal/stack/schema.json`](https://github.com/github/gh-stack/blob/main/internal/stack/schema.json).
- The schema requires `schemaVersion` to be `1` and defines structures for `stacks`, `BranchRef`, and `PullRequestRef` as implemented in [`internal/stack/stack.go`](https://github.com/github/gh-stack/blob/main/internal/stack/stack.go).
- File operations are atomic: `Load(dir)` validates and reads, while `Save(sf)` writes with `json.MarshalIndent` after acquiring a lock on `.git/gh-stack.lock`.
- Concurrency protection includes a 5-second timeout lock mechanism and checksum-based staleness detection to prevent data races.
- The `queued` field on `BranchRef` is transient (marked `json:"-"`) and never persists to disk, only existing in memory during merge queue operations.

## Frequently Asked Questions

### Where is the gh-stack metadata file located?

The file is located at `.git/gh-stack` relative to your repository root. This path is hardcoded in [`internal/stack/stack.go`](https://github.com/github/gh-stack/blob/main/internal/stack/stack.go) and referenced in [`AGENTS.md`](https://github.com/github/gh-stack/blob/main/AGENTS.md) as the canonical location for stacked branch metadata.

### What happens if two gh-stack commands run simultaneously?

The second command will fail with a `LockError`. The implementation in [`internal/stack/stack.go`](https://github.com/github/gh-stack/blob/main/internal/stack/stack.go) creates a `.git/gh-stack.lock` file with a 5-second timeout. If the lock cannot be acquired within this window, the operation aborts to prevent JSON corruption.

### Can I manually edit the .git/gh-stack file?

Manual editing is possible but risky. The `Save` function computes a checksum at load time; if the file changes on disk before your save operation completes, the system throws a `StaleError`. Any manual modifications must respect the JSON Schema in [`internal/stack/schema.json`](https://github.com/github/gh-stack/blob/main/internal/stack/schema.json) and avoid changing `schemaVersion` from `1`.

### How does gh-stack handle schema version mismatches?

If `Load` encounters a `schemaVersion` greater than the current constant, it aborts with an error message stating the file was written by a newer version of the tool. This prevents older binaries from corrupting data structures they do not recognize, forcing users to upgrade gh-stack to interact with newer stack files.