---
description: "Span name grammar, anchor syntax, the .span/ storage model, exit codes, and configuration."
---

# Reference (/docs/reference)

## Span names

A span name is one or more kebab-case segments separated by `/`: `<slug>`, `<category>/<slug>`, or `<category>/<subcategory>/<identifier-slug>`. Each segment may contain only lowercase `a-z`, `0-9`, and `-`, and must start with a letter or digit — no uppercase, no leading dot, no underscores.

```
$ git span add BadName web/f.ts
error: invalid name: `BadName` segment `BadName` must start with a-z or 0-9 (kebab-case segments separated by `/` (e.g. `<slug>`, `<category>/<slug>`, or `<category>/<subcategory>/<identifier-slug>`); lowercase a-z, 0-9, and `-`; each segment must start with a letter or digit)
```

A number of names are reserved because they collide with subcommands or other tokens, including `add`, `remove`, `commit`, `why`, `restore`, `revert`, `delete`, `move`, `drift`, `tree`, `fetch`, `push`, `doctor`, `log`, `config`, `list`, `help`, `pre-commit`, `advice`, `rewrite`, and `hooks`. This list is not exhaustive.

## Anchors

An anchor is either:

* A whole-file anchor: a bare `<path>`.
* A line-range anchor: `<path>#L<start>-L<end>`, 1-indexed and inclusive.

A line-range anchor whose end exceeds the file's current line count is rejected:

```
$ git span add wsspan web/f.ts#L100-L200
error: invalid anchor: end=200 exceeds file line count (7)
```

The range must be well-formed, with start ≤ end. A reversed range like `#L6-L4` fails anchor parsing with a misleading complaint about a missing `L` prefix — see [Re-anchor after an edit](/docs/guides/re-anchor-after-an-edit).

Before writing any `#L<start>-L<end>` range by hand, run `wc -l <path>` and check the end line against the file's current length. To cover an entire file, prefer the bare-path form over a full-file line range — it stays valid as the file grows.

## The `.span/` directory

> [!NOTE]
> Treat `.span/` as internal. Inspect span state through `git span show`, `list`, `drift`, and `history` — never by reading files under `.span/` directly or reconstructing state from `git log`/`git show`. Those commands are the source of truth for what's current; the raw files are an implementation detail that can change.

At a high level, the span root holds one file per span, named after its slug — a nested name like `category/slug` creates a subdirectory. Each span file holds one `<path>#L<start>-L<end> <algorithm>:<hash>` line per anchor, a blank line, then the why as trailing prose.

A handful of helper files live alongside the span files:

* `.gitattributes` — forces LF line endings for span files.
* `.gitignore` — ignores generated runtime artifacts (log files) so they don't get tracked. Still written by `ensure_span_dir`; the current in-session hooks (see [Agent integration](/docs/agent-integration)) keep their session state outside `.span/`, so the file is harmless to keep around.
* `.hookignore` — path-scoped suppression rules for the touch hook's context injection.
* `.advisorignore` — user-owned (nothing auto-creates it); each non-comment line is a gitignore-style path pattern, same grammar as `.hookignore`, excluding matching paths from the commit advisor's uncovered-writes check.

`git span show <name>` (and the bare `git span <name>` form) renders a friendlier TOML-style display — `name`, `message`, `[[anchors]]` blocks, a `[config]` block — but that's a display transform, not the on-disk format.

## Exit codes

| Command                    | Exit code | Meaning                                                                                                                                                                           |
| -------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Any subcommand             | 0         | Success                                                                                                                                                                           |
| `drift`                    | 0         | No drift found, or `--no-exit-code` passed while drift is present                                                                                                                 |
| `drift`                    | 1         | Drift found (`Changed`/`Moved` anchors), or an interior-anchor integrity violation                                                                                                |
| `drift`                    | 2         | Resolver `index_changed` verdict — the scan could not determine drift state; retryable                                                                                            |
| `add` / `why` (write mode) | 0         | Write succeeded and the post-write check completed and found no actionable drift                                                                                                  |
| `add` / `why` (write mode) | 1         | Write succeeded but actionable drift remains, or the post-write check errored — the output says which (remains lines vs `state unverified`; JSON `DRIFT` vs `UNKNOWN` + `reason`) |
| `add` / `why` (write mode) | 2         | Post-write check indeterminate (`index changed during check`) — retryable                                                                                                         |
| `add` / `why` / etc.       | 1         | Operational or validation error (invalid name, out-of-range anchor, and similar); `why --format json` in read mode is also rejected fail-closed with exit 1                       |
| Any command                | 2         | A clap usage error — bad flag, missing required argument                                                                                                                          |
| `merge-driver`             | 0         | Fully resolved                                                                                                                                                                    |
| `merge-driver`             | 1         | Partial resolution (residue markers written)                                                                                                                                      |

The interior-anchor integrity case is never maskable by `drift --no-exit-code` — it always exits 1, even when no anchor drifted.

## Published schemas

Every subcommand that accepts `--format json` publishes its output family as a JSON Schema at a stable URL, derived from the same Rust types that serialize the CLI output:

| Family   | URL                                                 | Subcommands  |
| -------- | --------------------------------------------------- | ------------ |
| mutation | `https://git-span.com/schemas/cli/v1/mutation.json` | `add`, `why` |
| resolve  | `https://git-span.com/schemas/cli/v1/resolve.json`  | `resolve`    |
| context  | `https://git-span.com/schemas/cli/v1/context.json`  | `context`    |
| history  | `https://git-span.com/schemas/cli/v1/history.json`  | `history`    |
| drift    | `https://git-span.com/schemas/cli/v1/drift.json`    | `drift`      |

Each schema's `$id` is its URL, and the URL is the version: v1 is immutable. A committed `sha256-manifest.txt` beside the schemas pins every published file's digest, and CI recomputes and compares it — the manifest is never regenerated, so published bytes cannot drift. A breaking change to a family's shape lands at a `v2` URL; the v1 bytes stay byte-for-byte what they were. The frozen bytes freeze the key set with them: a validator cached against a v1 schema stays correct for every output that family produces, because a round-trip test validates real CLI stdout against the committed schemas — a schema the binary violates fails CI, not a consumer's validator.

`replace` and `tree` also accept `--format json`, but their output is unversioned and unpublished: publishing would require adding a `schema_version` key to it, a change to an existing output shape. Their JSON remains an internal interface.

The schemas and the [command reference](/docs/commands) are generated from the CLI itself (`yarn build:schemas` in `packages/git-span`); a staleness gate fails CI when a committed artifact falls behind the generator.

## Configuration

**Span root directory.** Resolved with this precedence:

1. `--span-dir <path>` CLI flag
2. `GIT_SPAN_DIR` environment variable
3. `git config git-span.dir`
4. Default: `.span`

**Other environment variables and config:**

| Setting                  | Effect                                                                                                      |
| ------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `GIT_SPAN_PERF`          | Enables `--perf` timing output to stderr without passing the flag (`1`/`true`/`yes`/`on`, case-insensitive) |
| `GIT_SPAN_CACHE=0`       | Disables the on-disk resolver cache entirely                                                                |
| `GIT_SPAN_RENAME_BUDGET` | Overrides the rename-detection budget used by history/move tracking. Default `1000`                         |

The resolver cache lives in the git common directory and is safe to delete.

See [Command reference](/docs/commands) for per-subcommand flags and behavior.
