Skip to main content
Glama
README.md
# gitlore

[![CI](https://github.com/narek-keshishyan/gitlore/actions/workflows/ci.yml/badge.svg)](https://github.com/narek-keshishyan/gitlore/actions/workflows/ci.yml)
[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Read-only, structured access to a Git repository's history — as an **MCP server**
and as a **CLI**.

An agent reading your codebase sees only the current state. It cannot tell a
deliberate workaround from an accident, a hot spot from a stable file, or who to
ask about a subsystem. All of that is in the Git history; gitlore exposes it as
seven typed tools instead of handing the model a shell.

## Tools

| Tool | Answers |
|---|---|
| `search_commits` | When was this introduced, or removed? |
| `file_history` | How has this file evolved? (follows renames) |
| `blame` | Which commit put this line here, and why? |
| `churn` | Which files change most — where does risk concentrate? |
| `contributors` | Who knows this code? What is the bus factor? |
| `commit_detail` | What exactly did this commit do? |
| `what_changed_between` | What is different between these two refs? |

## Install

```bash
pip install gitlore
```

## Use it as a CLI

Every MCP tool is also a subcommand, so you can try the whole thing without an
MCP client:

```bash
gitlore who                              # contributors and bus factor
gitlore churn --since "6 months ago"     # where the change concentrates
gitlore history src/app/auth.py          # a file's evolution, through renames
gitlore blame src/app/auth.py -s 40 -e 60
gitlore search -q "rate limit" --author alice
gitlore between v1.2.0 v1.3.0
gitlore show 9f2c1ab
```

Add `--json` to any command for machine-readable output, and `--repo/-r` to
point at a repository other than the working directory.

## Use it as an MCP server

```bash
gitlore serve --repo /path/to/repo
```

For Claude Code:

```bash
claude mcp add gitlore -- gitlore serve --repo /path/to/repo
```

Or, in a client that reads a JSON config:

```json
{
  "mcpServers": {
    "gitlore": {
      "command": "gitlore",
      "args": ["serve", "--repo", "/path/to/repo"]
    }
  }
}
```

All seven tools are annotated `readOnlyHint: true` and `openWorldHint: false`,
so a host can auto-approve them without prompting a human on every call.

## Security

gitlore is driven by a language model whose context contains text written by
strangers — commit messages, branch names, paths in a repository someone else
authored. Every argument is treated as hostile.

**No shell.** `subprocess.run([...], shell=False)` with an argument list. There
is no string interpolation into a command line anywhere in the codebase.

**Allowlisted subcommands.** Only read-only git subcommands run; anything else
raises before a process is spawned. A denylist would be the wrong shape — git
has well over a hundred subcommands and gains more each release.

**Option-injection defence.** A path or ref beginning with `-` is rejected. This
is the check that gets left out: a "filename" of
`--output=/home/user/.ssh/authorized_keys` is not a filename, and
`--upload-pack=…` is a remote-code-execution primitive. Because arguments are
passed in an argv list, quoting does not help — only refusing does. Paths also
go after a `--` separator so git cannot reinterpret them as revisions.

**Path confinement.** Every path is resolved and checked to be inside the
repository root, so `..` cannot walk out.

**Hostile-repository hardening.** Git runs commands out of a repository's *own*
config — `core.fsmonitor`, `core.hooksPath`, `core.pager`, `diff.external`.
Pointing a naive tool at a repository someone else wrote is enough to execute
their code. Every invocation neutralises those with `-c` overrides and runs in a
scrubbed environment: inherited `GIT_*` variables are dropped, system and global
config are disabled, and terminal prompting is off so git can never block on a
credential prompt.

**Bounded output.** Timeouts on every invocation, byte caps at the subprocess
boundary, and record caps at the tool boundary. An unbounded `git log` would
exhaust the agent's context window — a denial of service against the thing
gitlore exists to help.

**Pinned repository.** The repository is fixed when the server starts and no
tool takes a repository argument, so a model cannot be talked into reading
somewhere else. There is no parameter through which to ask.

`tests/test_security.py` is adversarial and asserts all of the above.

## Development

```bash
uv venv && uv pip install -e ".[dev]"
pytest              # fixtures build real git repos; nothing about git is mocked
ruff check .
mypy
```

Tests build real repositories with `git init` and real commits at controlled
timestamps, then assert against real git output. Mocking git would only prove
the mock matches the code's assumptions, which is the thing in doubt.

[`docs/design.md`](docs/design.md) covers the architecture and the reasoning.

## License

MIT — see [LICENSE](LICENSE).