gitlore
README.md
# gitlore
[](https://github.com/narek-keshishyan/gitlore/actions/workflows/ci.yml)
[](https://www.python.org/)
[](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).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues