# Agent Session Data Linting Specification

**Version:** 1.0.0-draft
**Date:** 2026-04-08
**Maintained by:** [Yaw Labs](https://yaw.sh) / [ctxlint](https://github.com/YawLabs/ctxlint)
**License:** CC BY 4.0

---

## What is this?

AI coding agents persist session data -- command history, memory files, learned preferences -- across projects. When a developer runs the same agent across multiple repositories, these files accumulate state that shapes future behavior. Secrets get set in some repos but not others. Config files drift apart. Memory entries go stale or duplicate across projects.

This specification defines a standard set of lint rules for validating agent session data for cross-project consistency. It does NOT define agent session formats (those are owned by each agent vendor). Instead, it defines what to CHECK across the session data that already exists.

The specification includes:
- A reference of session data locations across 8 AI coding agents
- 12 lint rules in the `session` category with defined severities
- A machine-readable rule catalog ([`agent-session-lint-rules.json`](./agent-session-lint-rules.json))
- Sibling-repo detection for cross-project checks

This is the third pillar alongside context file linting (`CLAUDE.md`, `.cursorrules`) and MCP config linting (`.mcp.json`). Together they cover everything that shapes what an AI agent knows and can do:

| Pillar | What it checks | Specification |
|---|---|---|
| Context files | Instructions the agent reads | [CONTEXT_LINT_SPEC.md](./CONTEXT_LINT_SPEC.md) |
| MCP configs | Tools the agent can use | [MCP_CONFIG_LINT_SPEC.md](./MCP_CONFIG_LINT_SPEC.md) |
| Session data | History and memory the agent carries | This document |

**Reference implementation:** [ctxlint](https://github.com/YawLabs/ctxlint) (v0.7.0+)

---

## Table of contents

- [1. Agent Session Data Landscape Reference](#1-agent-session-data-landscape-reference)
  - [1.1 What is agent session data?](#11-what-is-agent-session-data)
  - [1.2 Data sources by agent](#12-data-sources-by-agent)
  - [1.3 Scan targets](#13-scan-targets)
  - [1.4 Sibling detection](#14-sibling-detection)
- [2. Lint Rules](#2-lint-rules)
  - [2.1 session/missing-secret](#21-sessionmissing-secret)
  - [2.2 session/diverged-file](#22-sessiondiverged-file)
  - [2.3 session/missing-workflow](#23-sessionmissing-workflow)
  - [2.4 session/stale-memory](#24-sessionstale-memory)
  - [2.5 session/duplicate-memory](#25-sessionduplicate-memory)
  - [2.6 session/consecutive-repeat](#26-sessionconsecutive-repeat)
  - [2.7 session/cyclic-pattern](#27-sessioncyclic-pattern)
  - [2.8 session/memory-index-overflow](#28-sessionmemory-index-overflow)
  - [2.9 session/shared-temp-path](#29-sessionshared-temp-path)
  - [2.10 session/unverified-gate-claimed-clean](#210-sessionunverified-gate-claimed-clean)
  - [2.11 session/default-branch-accumulation](#211-sessiondefault-branch-accumulation)
  - [2.12 session/unresolvable-sha](#212-sessionunresolvable-sha)
- [3. Rule Catalog (machine-readable)](#3-rule-catalog-machine-readable)
- [4. Implementing This Specification](#4-implementing-this-specification)
- [5. Contributing](#5-contributing)

---

## 1. Agent Session Data Landscape Reference

### 1.1 What is agent session data?

Agent session data is persistent state that carries across conversations. Unlike context files (which are authored by developers) and MCP configs (which are authored once and committed), session data is generated by the agent itself during use. It includes:

- **History files** -- a log of commands the agent executed, prompts it received, or actions it took. Typically appended per-conversation.
- **Memory files** -- notes the agent writes to itself about the project: conventions it learned, decisions it recorded, file paths it considers important.
- **Session transcripts** -- full conversation logs including all messages, tool calls, and responses. These can be very large (hundreds of megabytes for active projects).
- **Learned preferences** -- implicit or explicit settings derived from user behavior: preferred tools, naming conventions, workflow patterns.

This data is usually stored in the user's home directory, not in the project repository. It is rarely version-controlled and is often in undocumented or unstable formats.

### 1.2 Data sources by agent

| Agent | Provider | History Location | Format | Memory / Preferences |
|---|---|---|---|---|
| Claude Code | Anthropic | `~/.claude/history.jsonl` | JSONL | `~/.claude/projects/*/memory/*.md` (Markdown with YAML frontmatter) |
| Codex CLI | OpenAI | `~/.codex/history.jsonl` | JSONL | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` |
| Aider | Paul Gauthier | `.aider.chat.history.md` (per-project) | Markdown | `.aider.input.history` (per-project, readline format) |
| Vibe CLI | Mistral | `~/.vibe/sessions/*/messages.jsonl` | JSONL | `~/.vibe/sessions/*/meta.json` |
| Amazon Q | AWS | `~/.aws/amazonq/history/chat-history-*.json` | JSON | Keyed by workspace path hash |
| Goose | Block | `~/.local/share/goose/sessions/sessions.db` (Linux/macOS), `%APPDATA%\Block\goose\data\sessions\` (Windows) | SQLite | Schema v9, versioned migrations |
| Continue.dev | Continue | `~/.continue/sessions/*.json` | JSON | `~/.continue/dev_data/*.jsonl` |
| Windsurf | Codeium | `~/.windsurf/transcripts/*.jsonl` | JSONL | `~/.codeium/windsurf/cascade/` |

**Platform notes:**
- On Windows, `~` refers to `%USERPROFILE%` (typically `C:\Users\<name>`).
- Goose uses platform-specific paths: XDG on Linux, `~/Library/Application Support/` on macOS, `%APPDATA%` on Windows.
- Aider stores history in the project directory itself, not in the user's home directory. These files are typically gitignored.

**Format stability:**
- Claude Code's JSONL history and Markdown memory files are the most stable and well-documented formats.
- Codex CLI's JSONL format is documented via OpenAI's CLI docs.
- Goose uses SQLite with versioned schema migrations (v9 as of April 2026), making the format stable but requiring a SQLite dependency to read.
- Amazon Q, Vibe CLI, Windsurf, and Continue.dev formats are largely undocumented and may change without notice.

### 1.3 Scan targets

Session data varies enormously in size and format. This specification targets only lightweight, high-signal data sources for scanning.

**What we scan:**

- **History files** -- command logs in JSONL or Markdown format. Typically under 2 MB per project. Parsed line-by-line for specific patterns (e.g., `gh secret set` commands).
- **Memory files** -- small Markdown files, typically under 1 MB total per project. Parsed for path references and content overlap.
- **Sibling repo filesystem** -- file existence checks and file content reads for cross-project comparisons (diverged configs, missing workflows). No deep scanning.

**What we explicitly DO NOT scan:**

- **Full session transcripts** -- too large. Active projects can accumulate hundreds of megabytes of transcript data. Scanning these would be slow and yield low-signal results.
- **SQLite databases** -- Goose's `sessions.db` requires a SQLite dependency. Out of scope for v1. Future versions may add opt-in SQLite support.
- **File history or shell snapshots** -- some agents capture filesystem state or shell output. These are agent-internal data and not useful for cross-project linting.

### 1.4 Sibling detection

Several rules in this specification compare the current project against "sibling" repositories -- other projects by the same developer or team. Sibling detection determines which repos to compare against.

**Default strategy:** scan the parent directory of the current project. Filter to directories that contain at least one project indicator file:

- `.git`
- `package.json`
- `Cargo.toml`
- `go.mod`
- `pyproject.toml`

Skip hidden directories (starting with `.`) and `node_modules`.

**Overflow strategy:** if the parent directory contains more than 50 candidate projects, narrow the set by parsing `git remote get-url origin` in each candidate and filtering to repositories in the same GitHub organization or user namespace. This prevents false positives in shared development directories (e.g., `~/src/` containing forks from dozens of orgs).

**Configuration:** implementors should allow users to override sibling detection with an explicit list of paths if the default heuristics don't match their workspace layout.

---

## 2. Lint Rules

12 rules in 1 category (`session`). All rules in this category perform cross-project checks using sibling detection or per-project history analysis.

Severity levels:
- **error** -- the session data reveals a verifiably missing configuration. Should fail CI.
- **warning** -- the session data reveals likely drift worth investigating. May fail CI in strict mode.
- **info** -- the session data reveals a potential improvement. Never fails CI.

### 2.1 session/missing-secret

Detects GitHub secrets that have been set on sibling repositories but not the current project.

| Field | Value |
|---|---|
| **Rule ID** | `session/missing-secret` |
| **Severity** | error |
| **Trigger** | `gh secret set <NAME>` found in agent history for 2+ sibling repos but not the current project |
| **Message** | `GitHub secret "<name>" is set on <N> sibling repos (<names>) but not on this project` |

**Detection algorithm:**

1. Parse agent history files (JSONL) line-by-line.
2. Match entries against the pattern: `gh secret set <SECRET_NAME>` (with optional flags like `--repo`, `--body`, `--env`).
3. Group matches by secret name and the project path the history entry is associated with.
4. For each secret name, check if it was set on 2+ sibling repos but not on the current project.
5. Flag each missing secret as an error.

**Example scenario:** A developer uses Claude Code across three repos. They ran `gh secret set NPM_TOKEN` in `lib-a` and `lib-b` but forgot `lib-c`. When linting `lib-c`, this rule flags the missing `NPM_TOKEN`.

### 2.2 session/diverged-file

Detects canonical configuration files that have drifted between the current project and its siblings.

| Field | Value |
|---|---|
| **Rule ID** | `session/diverged-file` |
| **Severity** | warning |
| **Trigger** | A canonical file exists in both the current project and 1+ siblings, and line-level overlap is 20-90% |
| **Message** | `<file> has diverged from sibling repos: <sibling> (<N>% overlap)` |

**Canonical files:**

| File path | Purpose |
|---|---|
| `release.sh` | Release automation script |
| `.github/workflows/ci.yml` | CI pipeline |
| `.github/workflows/release.yml` | Release pipeline |
| `biome.json` | Biome linter config |
| `.prettierrc` | Prettier config |
| `.eslintrc.json` | ESLint config |
| `tsconfig.json` | TypeScript config |
| `.gitignore` | Git ignore rules |

**Detection algorithm:**

1. For each canonical file that exists in the current project, check if the same file exists in any sibling repo.
2. For each pair, compute line-level overlap: the number of lines present in both files divided by the total number of unique lines across both files, expressed as a percentage.
3. Classify the result:
   - **Below 20%** -- intentionally different. No flag. The files serve different purposes despite the shared name.
   - **20-90%** -- drift. Flag as warning. The files were likely once in sync and have diverged.
   - **Above 90%** -- close enough. No flag. Minor differences are expected (e.g., different project names).

**Notes:**
- Lines are trimmed before comparison; blank lines and very short lines (3 characters or fewer after trimming) are excluded from the overlap calculation. Comment lines count toward overlap -- drift in the comments of a canonical file is still drift.
- Compare each sibling independently. Report the sibling with the lowest overlap percentage first (the furthest-drifted sibling is the one worth reading first).

### 2.3 session/missing-workflow

Detects GitHub Actions workflow files that exist across sibling repos but are absent from the current project.

| Field | Value |
|---|---|
| **Rule ID** | `session/missing-workflow` |
| **Severity** | warning |
| **Trigger** | A GitHub Actions workflow file exists in 2+ sibling repos but not in the current project (which has a `.github` directory) |
| **Message** | `GitHub Actions workflow "<file>" exists in <N> sibling repos (<names>) but not in this project` |

**Detection algorithm:**

1. Check if the current project has a `.github` directory. If not, skip this rule entirely -- the project may not use GitHub Actions at all.
2. Enumerate `.github/workflows/*.yml` and `.github/workflows/*.yaml` in all sibling repos.
3. Group workflow filenames across siblings.
4. For each filename that exists in 2+ siblings but not in the current project, flag it.

**Notes:**
- Match by filename only, not by content. A `ci.yml` in one repo may be very different from `ci.yml` in another, but the absence of any CI workflow is still worth flagging.
- Exclude workflow files that are clearly project-specific (e.g., containing the repo name in the filename). Implementors may use heuristics here.

### 2.4 session/stale-memory

Detects Claude Code memory entries that reference file paths which no longer exist in the project.

| Field | Value |
|---|---|
| **Rule ID** | `session/stale-memory` |
| **Severity** | info |
| **Trigger** | A memory file references file paths that no longer exist in the project |
| **Message** | `Memory "<name>" references <N> path(s) that no longer exist: <paths>` |

**Scope:** This rule only checks memory files for the current project. Claude Code stores per-project memories in `~/.claude/projects/<encoded-path>/memory/`, where `<encoded-path>` encodes the project's absolute path (each of `:`, `/`, `\`, and `.` becomes a single `-` -- see [Section 4](#4-implementing-this-specification)).

**Detection algorithm:**

1. Determine the current project's encoded path (see [Section 4](#4-implementing-this-specification) for encoding details).
2. Read all `.md` files in the corresponding memory directory.
3. Extract path references from each memory file using the same path extraction logic as context file linting (forward-slash-separated segments, relative paths, etc.).
4. Check each extracted path against the project filesystem.
5. Flag memory files that reference 1+ paths that no longer exist.

**Notes:**
- This rule is `info` severity because stale memories are low-risk -- they waste a small amount of context but don't cause incorrect behavior.
- Implementors may suggest `claude memory remove` or manual deletion as a fix.

### 2.5 session/duplicate-memory

Detects memory entries from different projects that have significant content overlap.

| Field | Value |
|---|---|
| **Rule ID** | `session/duplicate-memory` |
| **Severity** | info |
| **Trigger** | Two memory entries from different projects have >60% line overlap |
| **Message** | `Memory "<nameA>" (<projA>) and "<nameB>" (<projB>) have <N>% overlap` |

**Detection algorithm:**

1. Enumerate all memory files across all projects in `~/.claude/projects/*/memory/*.md`.
2. Exclude `MEMORY.md` index files (these are auto-generated summaries, not authored memories).
3. Exclude very short entries (fewer than 50 characters after stripping whitespace) -- too short for meaningful overlap comparison.
4. Perform pairwise comparison of all remaining memory entries. Skip pairs from the same project, and skip pairs where neither side belongs to the current project -- without that scoping, every lint run from any repo would resurface the same unrelated cross-project duplicates.
5. Compute line-level overlap percentage (same algorithm as `session/diverged-file`, with a slightly higher trivial-line floor: lines of 5 characters or fewer after trimming are excluded).
6. Flag pairs with >60% overlap.

**Notes:**
- This rule helps identify boilerplate that has been memorized per-project instead of being placed in a shared context file or user-level config.
- A common pattern is the same coding conventions memorized independently in 5+ projects. Consolidating to a user-level `CLAUDE.md` or `.claude/settings.json` would be more efficient.

---

### 2.6 session/consecutive-repeat

Detects when an agent runs the same command 3 or more times consecutively, indicating a loop.

| Field | Value |
|---|---|
| **Rule ID** | `session/consecutive-repeat` |
| **Severity** | warning |
| **Trigger** | 3+ consecutive history entries with identical `display` values within a single session segment for the current project |
| **Message** | `Command run <N> times consecutively: "<command>"` |

**Detection algorithm:**

1. Filter history entries to the current project path (normalized). Drop entries with no associated project path, and entries with no timestamp (readers default a missing timestamp to 0; the gap split in step 3 keys off real timestamps, and an all-zero pseudo-session would never split, so a routine daily one-shot command would read as a 3+ repeat).
2. Sort entries by timestamp. Implementations may bound the scan to the most recent entries (the reference implementation keeps the latest 5,000) -- a live loop is always captured in the tail, and the cycle scan in `session/cyclic-pattern` is O(N²).
3. Group entries into sessions keyed by provider + session ID (session IDs are only unique within a provider). Split each session's sequence wherever the gap between consecutive timestamps exceeds 30 minutes -- providers that omit session IDs would otherwise pool unrelated working stints into one pseudo-session, and a routine daily one-shot command would read as a 3+ repeat.
4. Merge single-command sessions (sessions whose entire history is one entry) per provider into one chronological stream, split at the same >30-minute gaps. A rapid respawn loop (a headless one-shot command re-spawned every few seconds) produces N one-command sessions that are each below the threshold on their own; the merge keeps that pathology detectable, while runs more than 30 minutes apart (daily routine reuse) still split into separate below-threshold segments. The merged stream feeds only this rule, not `session/cyclic-pattern`.
5. Within each segment (per-session and merged one-shot), slide a window over the entries. For each run of 3+ entries with identical `display` values, emit a warning.

**Notes:**
- Looping is an intra-session pathology. Pooling full sessions would flag routine reuse across days, and concurrently interleaved sessions (including cross-provider ones, since multiple providers' histories are merged) would produce phantom patterns no session actually ran. The single-command-session merge in step 4 is the deliberate exception: N identical one-shots inside a 30-minute window are a respawn loop, not reuse.
- Truncates long command strings to 80 characters in the message for readability.
- This rule helps surface cases where an agent is stuck retrying a failing command instead of changing approach.

---

### 2.7 session/cyclic-pattern

Detects short repeating cycles of commands, indicating an agent stuck in a loop.

| Field | Value |
|---|---|
| **Rule ID** | `session/cyclic-pattern` |
| **Severity** | warning |
| **Trigger** | A sequence of 2-3 distinct commands repeating 2+ times consecutively (e.g. A,B,A,B) within a single session segment |
| **Message** | `Cyclic pattern repeated <N> times: <cycle>` |

**Detection algorithm:**

1. Build per-session segments exactly as in `session/consecutive-repeat` steps 1-3 (current project only, sorted by timestamp, grouped by provider + session ID, split at >30-minute gaps).
2. Within each segment, for cycle lengths 2 and 3, slide a window checking if the next `cycleLen` entries match the current cycle.
3. Cycles where every element is the same are excluded (already caught by `session/consecutive-repeat`).
4. A cycle whose span overlaps a run already reported by `session/consecutive-repeat` is suppressed -- those commands were already reported once.
5. Subsumption: if a shorter cycle is fully contained within an already-reported longer cycle at the same position, skip it.

**Notes:**
- A cycle like "edit file → run tests → edit file → run tests" is a common pattern when an agent is making iterative fixes. This rule flags when the cycle repeats enough times to suggest the agent isn't making progress.
- The suggestion directs users to check if a context file is missing workflow instructions.

---

### 2.8 session/memory-index-overflow

Detects when `MEMORY.md` exceeds Claude Code's session-load cap. Claude Code loads the first 200 lines OR 25KB of `MEMORY.md` at session start — whichever comes first. Entries past the cap are silently dropped, so auto-memory pointers beyond that point are effectively invisible to the agent.

| Field | Value |
|---|---|
| **Rule ID** | `session/memory-index-overflow` |
| **Severity** | warning |
| **Trigger** | `~/.claude/projects/<encoded-project>/memory/MEMORY.md` exceeds 200 lines OR 25,600 bytes |
| **Message (lines)** | `MEMORY.md has <N> lines — only the first 200 are loaded. <excess> line(s) are effectively invisible.` |
| **Message (bytes)** | `MEMORY.md is <N> bytes — only the first 25,600 bytes are loaded. ~<excess> bytes are effectively invisible.` |
| **Source** | [code.claude.com/docs/en/memory](https://code.claude.com/docs/en/memory) |

**Detection algorithm:**

1. Resolve `MEMORY.md` via the Claude-encoded project directory: `~/.claude/projects/<encode(currentProject)>/memory/MEMORY.md`.
2. If the file doesn't exist, no-op.
3. Count lines and bytes. Emit a warning for each dimension that exceeds its cap.

**Notes:**
- Each MEMORY.md entry should stay under ~150 characters (one-line pointer, not content).
- The remediation is to trim older entries, consolidate duplicates, or move detail into the corresponding topic file — topic files stay on-demand and don't count toward the cap.
- Both line and byte caps can fire independently (a short file with very long lines trips the byte cap first; a long file with short lines trips the line cap first).

---

### 2.9 session/shared-temp-path

Detects a fixed, non-session-scoped temp path that the agent **writes** and later **reads back**. `/tmp` is process-global, and under Git Bash on Windows it is shared across every concurrent agent session on the machine. An agent that backs a file up to a literal path, mutates the original, then restores from that path is racing every other session that picked the same obvious name.

| Field | Value |
|---|---|
| **Rule ID** | `session/shared-temp-path` |
| **Severity** | error |
| **Trigger** | Session history writes a literal path under `/tmp`, `/var/tmp`, `$TMPDIR` or `%TEMP%` with no per-run component, then reads the same path |
| **Message** | `Fixed temp path "<path>" is written and later read back` |
| **Source** | Observed incident (see Notes) |

**Detection algorithm:**

1. Sort session history by timestamp so "written, then read" is a real ordering rather than mere co-occurrence.
2. For each entry, extract write targets (shell redirect, `cp`/`mv`/`copy`/`move` destination, `tee`, `writeFileSync`, `curl -o`) and read sources (`cp`/`copy` source, `cat`, `type`, `source`, `readFileSync`, `<` redirect).
3. A candidate qualifies only if it sits under a shared temp root AND carries no per-run component (`$$`, `$pid`, `$RANDOM`, a session id, or `mktemp` anywhere on the line).
4. Emit an error when a qualifying read is preceded by a qualifying write of the same normalized path. Report each path once.

**Notes:**
- The motivating incident: an agent measuring a packaging change wrote `package.json` to `/tmp/pkg.bak`, ran `npm pack --dry-run`, then restored with `cp /tmp/pkg.bak package.json`. Between the write and the restore, a concurrent session working a sibling repo used the same `/tmp/pkg.bak`. The restore wrote a **different** package's manifest into the repo — wrong name, version, `bin` and dependencies. A release from that tree would have published under the wrong identity.
- **The pair is the signal, not either half.** A scratch file that is never read back cannot be clobbered into the workspace, and a read with no matching write is consuming something another tool produced deliberately. Both are ignored.
- `mktemp` output and any path carrying a per-run component are deliberately **not** flagged — those are the correct form and appear constantly in the same transcripts, so flagging them would bury the real finding.
- Remediation is a per-run path (`T=$(mktemp)`) or a session-scoped scratch directory assigned by the harness.

---

### 2.10 session/unverified-gate-claimed-clean

Detects a session that asserts a quality gate **passed** while that gate's own invocation failed or produced nothing.

| Field | Value |
|---|---|
| **Rule ID** | `session/unverified-gate-claimed-clean` |
| **Severity** | warning |
| **Trigger** | A lint/typecheck/test/build command errors or emits no output, and nearby agent prose claims it passed |
| **Message** | `'<gate>' asserted as passing, but the invocation produced no output` |
| **Source** | Observed incident (see Notes) |

**Detection algorithm:**

1. Read the project transcript (see §3, "Data sources") and order events by timestamp.
2. Find gate-shaped commands (`biome`/`eslint`/`ruff`/`clippy`/`lint`, `tsc`/`typecheck`, `vitest`/`jest`/`pytest`/`test`, `build`) whose result was flagged `is_error` **or** produced neither stdout nor stderr.
3. Scan forward up to 12 events for agent prose asserting a pass (`clean`, `passing`, `all green`, `no violations`, `0 errors`).
4. Stop the scan early if the same gate is re-run — a later invocation supersedes the failed one — or if the prose labels the state honestly.
5. Emit a warning per gate when a claim is found.

**Notes:**
- The motivating session ran `biome check` roughly a dozen times. On that host (Windows ARM64) the binary segfaults during exit — via the npx wrapper, via the native `biome.exe`, and unchanged by shell — producing **zero bytes** every time. The agent twice reported this as "zero diagnostics emitted, which is consistent with a clean run." Disproving it took a deliberate experiment: the same binary against a file with an unused variable and mangled formatting *also* produced zero bytes. The crash precedes diagnostic emission, so empty output carries no information about cleanliness at all.
- Prose that labels the state honestly — "unverified", "could not verify", "crashed", "blocked", "inconclusive" — is deliberately **not** flagged. A session saying "lint is UNVERIFIED because the runner crashed" reached the correct conclusion. The rule targets the false claim, not the failed gate.
- Sibling to `commands/exit-status-masked`, which is the *static* half: it reads a documented command whose own shape discards the status (`npx tsc --noEmit | head -20 && echo "tsc clean"`). This rule is *dynamic* — it fires on a plain `pnpm lint` that crashed, a command with nothing structurally wrong with it.

---

### 2.11 session/default-branch-accumulation

Detects a session that accumulates edits on the repo's **default branch** without an intervening commit.

| Field | Value |
|---|---|
| **Rule ID** | `session/default-branch-accumulation` |
| **Severity** | warning |
| **Trigger** | Ten or more distinct files written while on `main`/`master` with no commit or branch-away in between |
| **Message** | `<count> files edited on '<branch>' with no intervening commit` |
| **Source** | Observed incident (see Notes) |

**Detection algorithm:**

1. Read the project transcript (see §3, "Data sources") and order events by timestamp.
2. Track the branch from the `gitBranch` stamp the harness writes on each record.
3. Accumulate distinct written paths (`Write`/`Edit`/`NotebookEdit`) while the branch is a default branch.
4. Reset the accumulator on `git commit` (excluding `--dry-run`) or on a branch-away (`git checkout -b`, `git switch -c`, `git worktree add`).
5. Emit a warning when the accumulated count reaches 10.

**Notes:**
- The motivating session ran for hours across review, fix, coverage and audit phases and edited 25 files. Every edit landed in the working tree of `main`, uncommitted, and it surfaced only during a ship-readiness audit at the very end — no git-shaped signal fired along the way.
- Two things make that worse than untidy. Repo operating instructions commonly say to branch before committing on the default branch, so the end state is one the session was told to avoid. And on a machine running a fleet of agents — the sibling repo this came from had 11 locked worktrees and a `main` whose `HEAD` moved three times during a single audit — a large uncommitted delta on a shared default branch is one `git checkout --` or `git stash` away from being someone else's cleanup.
- The defect is **accumulation**, not the first write. A one-line typo fix on `main` is normal, and flagging it would make the rule noise. Sessions that branch first or commit as they go stay clean.
- Writes with no observed branch stamp are not counted: a finding pinned to a branch that was never actually observed is worse than a miss.

---

### 2.12 session/unresolvable-sha

Detects a memory that cites a git SHA which no longer resolves in the repository.

| Field | Value |
|---|---|
| **Rule ID** | `session/unresolvable-sha` |
| **Severity** | warning |
| **Trigger** | A cue-preceded 7-40 character hex token outside code fences fails to resolve via `git cat-file -t` |
| **Message** | `cited commit <sha> does not resolve in this repository` |
| **Requires** | git |

**Detection algorithm:**

1. Scope to memories belonging to the current project (same scoping as §2.4).
2. Strip fenced code blocks — a SHA inside a fence is sample input, not a claim.
3. Remove full UUIDs before extraction, then match `\b[0-9a-f]{7,40}\b`.
4. Drop tokens that are not commit citations: all-decimal (ids, timestamps, dates), `#`-prefixed (hex colours), `0x`-prefixed, digest-prefixed (`sha256:`, `md5=`), adjacent to a hyphen or dot between word characters (UUID remnants, hashed filenames, dotted version fragments), and interior path segments.
5. Require a **citation cue** within 80 characters before the token on the same line — `commit`, `SHA`, `revision`, `HEAD`, `tag`, `branch`, `PR`, `landed`, `merged`, `shipped`, `introduced`, `reverted`, `cherry-picked`, `backported`, `fixed`, `removed`, `added`, `renamed`, `bumped`, `released`.
6. Resolve each distinct surviving token with `git cat-file -t`. Report only the ones that do **not** resolve. Bound the number of resolutions per run; an undecided token stays silent.
7. Without a git repository, report nothing.

**Notes:**
- `session/stale-memory` covers memories referencing dead *paths*. SHA citations rot faster: a squash-merge invalidates every SHA on the branch at once, and a rebase invalidates them silently. An agent that reads such a memory and runs `git show <sha>` gets `fatal: bad object` and has to re-derive the history it was told.
- **Shape is not enough, and resolution alone is not enough either.** Real memory corpora are full of hex-shaped tokens that are not commits — `originSessionId: 77bde817-610b-4f82-971d-1c2452b07917`, `image sha256:7e7b3ab9`, decimal product ids, and words that happen to be hex (`beadfaced`). None of them resolve, so a resolve-only rule reports every one. The cue requirement in step 5 is what makes the rule quiet; the resolution in step 6 is what makes the finding true.
- Do **not** try to filter by shape alone in the other direction either. A nine-character hex token that reads as an English word is indistinguishable from a short SHA by pattern; only resolution separates them.
- **Misattribution is deliberately out of scope.** The motivating instance cited a SHA that *does* resolve but is not the commit that made the change. Detecting that means comparing the commit's diff against the surrounding prose claim — a genuinely different, much fuzzier rule that must not be smuggled in under this ID.

---

## 3. Rule Catalog (machine-readable)

A machine-readable JSON catalog of all rules is available at [`agent-session-lint-rules.json`](./agent-session-lint-rules.json). It conforms to the shared catalog schema ([`schemas/ctxlint-catalog.schema.json`](./schemas/ctxlint-catalog.schema.json)) used by all four pillars: each rule entry carries `id`, `category`, `severity`, `description`, `trigger`, `message`, `fixable`, and `stability`, plus rule-specific extras (e.g. `canonicalFiles` on `session/diverged-file`).

See the JSON file for the full catalog.

### Catalog rule IDs vs. reference-implementation ruleIds

Catalog rule IDs use the pillar-stable `session/<slug>` form -- these are the cross-tool names to use in documentation, configuration, and issue reports. The reference implementation namespaces the `ruleId` it emits (in `--format json` output) by check module instead -- `<check>/<slug>` -- and splits `session/memory-index-overflow` into one emitted slug per cap dimension. The full correspondence (pinned by a consistency test in the reference implementation):

| Catalog rule ID | Emitted `ruleId` (reference implementation) |
|---|---|
| `session/missing-secret` | `session-missing-secret/missing-secret` |
| `session/diverged-file` | `session-diverged-file/diverged-file` |
| `session/missing-workflow` | `session-missing-workflow/missing-workflow` |
| `session/stale-memory` | `session-stale-memory/stale-memory` |
| `session/duplicate-memory` | `session-duplicate-memory/duplicate-memory` |
| `session/consecutive-repeat` | `session-loop-detection/consecutive-repeat` |
| `session/cyclic-pattern` | `session-loop-detection/cyclic-pattern` |
| `session/memory-index-overflow` | `session-memory-index-overflow/line-overflow`, `session-memory-index-overflow/byte-overflow` |
| `session/shared-temp-path` | `session-shared-temp-path/shared-temp-path` |
| `session/unverified-gate-claimed-clean` | `session-unverified-gate-claimed-clean/unverified-gate-claimed-clean` |
| `session/default-branch-accumulation` | `session-default-branch-accumulation/default-branch-accumulation` |
| `session/unresolvable-sha` | `session-unresolvable-sha/unresolvable-sha` |

### Data sources: history vs. transcript

Session rules read two distinct sources, and the difference decides what a rule can see.

**`~/.claude/history.jsonl`** records only what the **user typed** — one entry per prompt, with `display`, `timestamp`, `project` and `sessionId`. It carries no tool invocations, no command output and no git state.

**`~/.claude/projects/<encoded-project>/<uuid>.jsonl`** is the session **transcript**. It carries `tool_use` blocks with their inputs, the matching `tool_result` with `is_error` and output, and a `gitBranch` stamp on assistant records. Every rule whose signal is *what the agent did* — the command it ran, the gate that crashed, the branch the edits landed on — needs this source; a rule built on `history.jsonl` alone would be inert against them.

Transcript reads are scoped to the **current project** and bounded (most recent 5 transcripts, 200,000 lines), because the corpus on a working machine reaches hundreds of megabytes across a hundred-plus project directories. The bound is reported rather than applied silently, so a check cannot report "clean" off a truncated read.

Other implementations of this specification may emit either form; when interoperating, treat the catalog IDs as canonical and map implementation-specific ruleIds onto them as above.

---

## 4. Implementing This Specification

### Opt-in activation

Session checks are opt-in. Implementors should require an explicit flag (e.g., `--session`) to enable these rules. Rationale:

- Session data lives outside the project directory and may contain sensitive information.
- Cross-project checks require filesystem access to sibling repos, which is slower than single-project linting.
- Users should consciously opt into scanning their agent history and memory files.

### Path handling

Sibling detection and history file scanning must handle both Windows and Unix paths correctly.

- Use `path.resolve()` or equivalent to normalize paths before comparison.
- On Windows, handle both `\` and `/` as separators.
- Environment variable expansion (`%USERPROFILE%`, `$HOME`, `~`) should work on both platforms.

### History file parsing

Agent history files (JSONL) should be parsed line-by-line using streaming reads. Do not load entire files into memory -- active developers may have history files in the tens of megabytes.

For each line:
1. Parse as JSON.
2. Extract the command/action string.
3. Match against rule-specific patterns (e.g., `gh secret set` regex).
4. Track the associated project path for cross-project grouping.

### Claude Code project directory encoding

Claude Code encodes a project's absolute path into a directory name under `~/.claude/projects/` by replacing **each of `:`, `/`, `\`, and `.` with a single `-`**. Nothing is stripped: a leading `/` on Unix paths is preserved as a leading `-`. The familiar `--` run in Windows-derived names like `C--Users-...` is not a separator of its own -- it is the drive letter's `:` and the adjacent `/` each becoming `-`. Hyphens already present in a path component are preserved as-is.

**Examples:**

| Actual path | Encoded directory name |
|---|---|
| `C:/Users/jeff/yaw/ctxlint` | `C--Users-jeff-yaw-ctxlint` |
| `/home/dev/projects/my-app` | `-home-dev-projects-my-app` |
| `/Users/dev/work/api-server` | `-Users-dev-work-api-server` |
| `/home/dev/repo.js` | `-home-dev-repo-js` |

The encoding is **lossy**: `-`, `.`, `/`, `\`, and `:` all collapse to the same output character, so distinct paths can encode to the same directory name (`/home/dev/my-app` and `/home/dev/my.app` collide). There is no decode step. Implementors must compare encoded-to-encoded: encode the current project's absolute path with the same substitution rules and match the result against the directory names actually present in `~/.claude/projects/` -- never attempt to reconstruct a path from an encoded name.

### Scope of v1

The v1 specification focuses on:

- **Claude Code and Codex CLI** for history file scanning -- these have well-documented, stable JSONL formats.
- **Filesystem-based checks** (`session/diverged-file`, `session/missing-workflow`) that work for all agents because they scan the project filesystem, not agent-specific data.
- **Claude Code** for memory file checks (`session/stale-memory`, `session/duplicate-memory`) -- the only agent with a well-documented, file-based memory system.

Future versions may add support for:
- Goose SQLite session parsing (requires bundling or requiring SQLite).
- Aider per-project history parsing.
- Additional memory/preference formats as agents stabilize their storage.

---

## 5. Contributing

This specification is maintained at [github.com/YawLabs/ctxlint](https://github.com/YawLabs/ctxlint).

To propose changes:
- **New rules:** Open an issue describing the rule, its severity, trigger condition, and which agents it applies to.
- **Agent additions:** As new AI coding agents emerge or existing agents change their session data formats, submit a PR updating the data sources table in Section 1.2.
- **Corrections:** If any agent behavior or file location documented here is inaccurate, open an issue with evidence (agent docs, source code, or reproduction steps).

### Versioning

This specification follows semver:
- **Patch** (1.0.x): Typo fixes, clarifications, no rule changes
- **Minor** (1.x.0): New rules added, new agents documented, new canonical files for diverged-file checks
- **Major** (x.0.0): Rules removed or semantics changed in breaking ways

### Related specifications and tools

- [AI Context File Linting Specification](./CONTEXT_LINT_SPEC.md) -- context file lint rules (the first pillar)
- [MCP Server Configuration Linting Specification](./MCP_CONFIG_LINT_SPEC.md) -- MCP config lint rules (the second pillar)
- [ctxlint](https://github.com/YawLabs/ctxlint) -- reference implementation of all four specifications
- [mcp-compliance](https://github.com/YawLabs/mcp-compliance) -- tests MCP server behavior against the protocol spec
