# Unwritten

> Unwritten gives coding agents your team's unwritten rules. It mines git history to learn which files change together, then flags confident absences with evidence: "you changed A but not B — they co-change in 59 of 63 commits." No LLM, no config. IntelliSense for conventions: an MCP server for agents, plus a CLI for pre-commit hooks.

- **Type:** MCP server
- **Install:** `agentstack add mcp-byggarepop-unwritten`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Byggarepop](https://agentstack.voostack.com/s/byggarepop)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Byggarepop](https://github.com/Byggarepop)
- **Source:** https://github.com/Byggarepop/Unwritten

## Install

```sh
agentstack add mcp-byggarepop-unwritten
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Unwritten

[](https://www.nuget.org/packages/Unwritten)
[](https://www.nuget.org/packages/Unwritten)
[](LICENSE)

**The free, agent-native slice of change coupling.** Unwritten learns from your git
history which files are expected to change together, and flags statistically
confident *absences*: "you changed `OrderService.cs` but not
`OrderServiceTests.cs`, and they co-change 94% of the time."

It runs as an **MCP server** so AI coding agents (Claude Code, Copilot) can check
their own edits for holes mid-session, and as a **CLI** for pre-commit hooks.
One `dotnet tool execute`, an index in `.unwritten/`, no server, no subscription,
no tokens.

**Works on any language.** File-level rules only need git history, so hole
detection ("file A usually change with file B") works the same on Python, TypeScript, Go, or mixed repos. C# repos
additionally get method-level rules ("you changed `CalculateFreight` but not
its test") and cosmetic-edit filtering; JSON files get key-level noise
filtering. Running the tool requires the [.NET SDK](https://dotnet.microsoft.com/download)
(10+) — already present on any .NET dev machine — but the repos it analyzes
can be anything.

## This is not a new idea — and that's the point

Unwritten modernizes a 20-year-old research lineage for the agent era. The field is
called *change coupling* (or *evolutionary coupling*), studied extensively by the
Mining Software Repositories community:

- **ROSE (Zimmermann, Weißgerber, Diehl & Zeller, 2004–2005)** mined association
  rules from version history in an Eclipse plugin — "programmers who changed
  these functions also changed…" — explicitly to prevent errors from incomplete
  changes, including warnings about missing items.
- **CodeScene** (commercial) is the closest neighbor. Its delta analysis can
  also warn when an expected change pattern is broken — a file that usually
  changes with another is missing from a pull request. The difference is
  *where* and *how* it runs: CodeScene is a server product that analyzes pull
  requests after the fact, as part of a paid platform for code health,
  hotspots, and team analytics. Unwritten is one small free tool that answers
  the same kind of question *while the code is being written* — inside the
  coding agent's loop, before anything is committed — locally, with no server,
  and with every warning carrying its explicit confidence score and example
  commits as proof.

**What Unwritten adds:**

**Built for AI assistants.** Coding agents like Claude Code and Copilot can ask Unwritten "did I forget anything?" *while they work* — not after the pull request is opened. Older tools were built for humans clicking in an editor (ROSE, 2004) or review changes server-side after the fact (CodeScene).

**Free and simple.** Open source, runs on your machine, needs no server or account. One installed tool, one small index file in your repo — that's it.

**Honest about certainty.** If two files changed together 3 times out of 4, that could easily be coincidence. If it happened 90 times out of 100, it's a real pattern. Unwritten uses a statistical formula (the Wilson lower bound) that scores evidence based on both how often the pattern held *and* how much evidence there is:

- 3 out of 4 → raw ratio 75%, but confidence only **0.301** — too little evidence to trust
- 15 out of 17 → raw ratio 88%, confidence **0.657** — starting to look real
- 90 out of 100 → raw ratio 90%, confidence **0.826** — a real pattern
- 15 out of 15 → a perfect record, confidence **0.796** — a small project *can* earn high confidence, but perfection on 15 samples is still only 15 samples (it lands just under the 0.8 bar)

So more evidence is always worth more: 3 out of 4 and 90 out of 100 get very different scores, even though the percentages look similar. And when two patterns do end up with the same score, they've earned it — a small project needs a near-perfect record to reach what a large one reaches with a few exceptions.

Read more about it in [How it works](#how-it-works).

## Why these thresholds (tested on real data)

We tested the idea on EF Core, a large Microsoft project (13,642 commits). The tool learned 1,976 patterns from the older history, then flagged "holes" — expected files that were missing from a change — in the 2,047 newest commits. We then checked: was the missing file actually changed soon after (within 10 commits)? If yes, the warning was probably right.

| Confidence | holes flagged | turned out to be right |
|---|---|---|
| 0.3–0.5 | 1,429 | ~10% (basically noise) |
| 0.6 | 254 | 50% |
| 0.7 | 328 | 66.5% |
| 0.8 | 133 | 79.7% |

The pattern is clear: below 0.6 the warnings are mostly noise, above 0.6 they quickly become trustworthy. That's why these defaults are built in (don't change them without re-testing):

- Warnings start at confidence **0.6** — where the data shows warnings start being right. The CLI blocks a commit only at **0.7**.
- Commits touching more than 30 files are ignored during learning — big refactors and merges would teach false patterns.
- A file must have changed at least 10 times before rules about it are trusted.
- **Every warning comes with proof** — how many times the pattern held, and links to real example commits. A warning about something *missing* can't point at a file, so it has to bring its own evidence.

## Quick start

From your repo's root:

 1. Warm up the index (optional — every command builds it on first use and keeps it current by itself; this just makes the first query fast).
``` bash
dotnet tool execute Unwritten --yes -- reindex
```
 2. Register as an MCP server — see [As an MCP server](#as-an-mcp-server).

 3. (Recommended if using Claude Code) Make the check deterministic — a git pre-commit hook and a Claude Code Stop hook that feeds failing holes back to the agent before a commit actually is made:
 ```bash
dotnet tool execute Unwritten --yes -- install-hook --git --claude-code
```

That's it — your agent can now call `check_holes` after editing, and the hooks
catch the cases where it forgets to.

## Use

No install step — `dotnet tool execute` downloads the tool on first use and
runs it (the shorter alias `dnx` works too, but examples here spell it out):

```bash
dotnet tool execute Unwritten --yes -- check --staged
```

Everything before `--` is for the tool runner; everything after it is the
Unwritten command line.

The index lives in `.unwritten/`, a local cache rebuilt from history on demand.
The directory ships its own `.gitignore`, so nothing to add to yours. (To share
`.unwritten/config.json` with your team, commit it with `git add -f`.)

### As an MCP server

**Claude Code:**

```bash
claude mcp add unwritten -- dotnet tool execute Unwritten --yes -- mcp
```

**VS Code (Copilot Chat):** create or edit `.vscode/mcp.json` in your workspace:

```json
{
  "servers": {
    "unwritten": {
      "type": "stdio",
      "command": "dotnet",
      "args": ["tool", "execute", "Unwritten", "--yes", "--", "mcp"]
    }
  }
}
```

**Visual Studio 2026 (GitHub Copilot Chat):** the same `servers` snippet as VS Code,
in `%USERPROFILE%\.mcp.json` (all solutions) or a `.mcp.json` next to your solution
file — then restart Visual Studio so it loads the server.

Any other stdio MCP client: `dotnet` with
`["tool", "execute", "Unwritten", "--yes", "--", "mcp"]`.

**Through an MCP gateway** (e.g. [McpOrchestrator](https://github.com/Byggarepop/dotnet-mcp-orchestrator)):
the router's LLM only sees your capability description, so give it one that says
*when* to call Unwritten. Copy this as the capability's instructions:

```text
Unwritten — repo convention guard. Learns which files AND
code members (methods, classes) historically change together
(mined from git history, statistical confidence with evidence)
and flags expected-but-missing companion changes at both levels.

CALL THIS WHEN:
- You have made ANY code change in a git repo — modified a method,
  added a class, renamed something, edited config, created or
  deleted files — and are about to finish a task, commit, or hand
  back to the user → check_holes with the changed files (or omit
  files to auto-detect all uncommitted changes; pass baseRef =
  the pre-work commit if you committed during the session).
  Works at member level too: "you changed CalculateFreight but
  not its tests" — so call it even when only method bodies changed.
- You are planning a change and want to know which files or
  members usually accompany the one you're about to touch →
  expected_companions.
- A hole was flagged and you need to judge whether this case is a
  legitimate exception → explain_rule (returns historical evidence
  and past exceptions).
- The user asks "did I/you forget anything?", "what usually
  changes with this?", or mentions co-change/conventions →
  check_holes or expected_companions.
- Index maintenance: reindex after large history changes; stats
  for index health.

DO NOT CALL FOR: code quality/lint/style questions, test
execution, non-git workspaces, or questions about code *content*.
Warnings are patterns, not rules — high-confidence holes (≥0.7)
should be fixed or explicitly justified; lower ones are
suggestions.
```

Tools exposed:

| Tool | Purpose |
|---|---|
| `check_holes(repoPath, files?, minConfidence?, baseRef?)` | After editing, pass the files you touched — or omit `files` to auto-detect every uncommitted change. Returns expected-but-absent files with evidence, plus `checkedFiles` (how much history each input has: an empty result for a file with no history means "no data", not "all good"). Pass `baseRef` (the pre-work commit) when the session has already committed, so committed edits are still seen. |
| `expected_companions(file, repoPath, top=10)` | Ranked co-change companions of a file, regardless of threshold — proactive context when opening a file. |
| `explain_rule(fileA, fileB, repoPath)` | Full evidence for one rule: counts, confidence both directions, co-change commits, and recent commits where A changed alone (the exceptions). |
| `stats(repoPath)` | Index health: indexed HEAD, transaction/entity/pair counts, index size, rule counts at floors 0.5/0.6/0.7/0.8. |
| `reindex(repoPath)` | Force a full rebuild — after history rewrites or config changes. |

### As a CLI

```bash
dotnet tool execute Unwritten --yes -- check                     # all uncommitted changes
dotnet tool execute Unwritten --yes -- check --staged            # staged files (pre-commit hooks)
dotnet tool execute Unwritten --yes -- check --base abc1234      # everything changed since that commit
dotnet tool execute Unwritten --yes -- check src/Foo.cs src/Bar.cs    # an explicit file set
dotnet tool execute Unwritten --yes -- check --staged --fail-at 0.8   # stricter gate
```

Holes at or above the report floor (0.6) are printed; the exit code is 1 when
any hole reaches the fail floor (0.7). Checked files with no or too little
history are called out with `note:` lines — silence from a file the index has
never seen means "no data", not "no problems".

The index is fully self-maintaining: the first call on a repository indexes its
full history (seconds to a minute on large repos); afterwards only new commits
are ingested, config changes are picked up automatically, and queries are pure
in-memory lookups. You never need to run `reindex` yourself except after a
git history rewrite.

#### Actual examples from a demo
Checking stats of the index show 1 pair that goes above the 0.7 rule

Checking stats after a change has been made to one of the files.
The tool will inform that there is one file with a confidence score above 0.7 that is not changed together with its pair. And it will show you 3 ways you can take to resolve the issue.

Installing the pre-commit hook gives same output

### As hooks — deterministic checks

Relying on an agent (or a human) to remember to run the check is the weakest
link. `install-hook` makes it fire every time:

```bash
dotnet tool execute Unwritten --yes -- install-hook --git           # git pre-commit hook
dotnet tool execute Unwritten --yes -- install-hook --claude-code   # Claude Code Stop hook
```

- **`--git`** writes `.git/hooks/pre-commit` (refusing to touch an existing
  foreign hook unless `--force`).
- **`--claude-code`** adds a [Stop hook](https://docs.claude.com/en/docs/claude-code/hooks)
  to the repo's `.claude/settings.json`: whenever the agent finishes a turn
  while an uncommitted change has a failing hole, the report is fed back to the
  agent so it can fix or justify it. The hook fails open (never blocks on
  infrastructure problems) and never re-blocks within the same turn.

Manual pre-commit alternative — create `.git/hooks/pre-commit` (no extension) with:

```sh
#!/bin/sh
exec dotnet tool execute Unwritten --yes -- check --staged
```

and make it executable (`chmod +x .git/hooks/pre-commit`; not needed on
Windows). The commit is blocked when any hole reaches the fail floor (default
0.7); holes between 0.6 and 0.7 are printed as warnings but do not block. To
bypass once: `git commit --no-verify`.

With the [pre-commit](https://pre-commit.com) framework, add to
`.pre-commit-config.yaml`:

```yaml
repos:
  - repo: local
    hooks:
      - id: unwritten
        name: unwritten hole check
        entry: dotnet tool execute Unwritten --yes -- check --staged
        language: system
        pass_filenames: false
```

With [Husky](https://typicode.github.io/husky/): `echo "dotnet tool execute Unwritten --yes -- check --staged" > .husky/pre-commit`.

### Muting a false rule — `unwritten ignore`

Sometimes a high-confidence rule is simply wrong for your situation and keeps
blocking commits. Content-aware suppression already absorbs cosmetic C# and
JSON edits automatically, and `git commit --no-verify` bypasses a single
commit — but for a *persistently* false pairing, mute it:

```bash
dotnet tool execute Unwritten --yes -- ignore docs/api.md src/openapi.json --for 30 --note "generated separately now"
dotnet tool execute Unwritten --yes -- ignore --list
dotnet tool execute Unwritten --yes -- ignore --remove docs/api.md src/openapi.json
```

Ignores are **bounded by design** — permanent mutes go stale and one day hide a
real omission. An ignore expires after the trigger has changed `--for` more
times (default 30): those are exactly the commits that either erode the false
rule (each trigger-alone commit lowers its confidence, so it often dies
naturally before the mute expires) or prove the coupling is real again. Muted
holes still appear as `suppressed` with their remaining budget — never silently
dropped — and `check --strict` overrides them. Ignores live in
`.unwritten/ignores.json` (machine-managed; keep your hands in `config.json`).

There is deliberately no MCP tool for creating ignores: muting a warning is a
human judgment, not something a coding agent should do to its own findings.
The loop is still closed, though — the `check_holes`/`explain_rule` tool
descriptions and the Stop-hook feedback instruct the agent that when it judges
a rule persistently false, it must say so and point you at `unwritten ignore`,
so the decision reaches you instead of dying in the agent's transcript.

### Configuration — `.unwritten/config.json`

The first index build drops a fully commented template at
`.unwritten/config.json`, so the settings are discoverable right where they
live — uncomment a line to change it. All settings are optional; missing ones
use the validated defaults:

```jsonc
{
  "minSupport": 10,            // min changes of the trigger file before rules exist
  "maxTransactionSize": 30,    // commits touching more files are excluded from training
  "defaultMinConfidence": 0.6, // alert floor (check_holes + CLI report floor)
  "failConfidence": 0.7,       // CLI: exit 1 when a hole reaches this
  "maxExamplesPerPair": 10,    // evidence commits kept per file pair

  // Content-aware exceptions (JSON trigger files):
  "facetFloor": 0.6,           // Wilson floor for a key to count as predict

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Byggarepop](https://github.com/Byggarepop)
- **Source:** [Byggarepop/Unwritten](https://github.com/Byggarepop/Unwritten)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-byggarepop-unwritten
- Seller: https://agentstack.voostack.com/s/byggarepop
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
