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.
| 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? |
pip install gitloreEvery MCP tool is also a subcommand, so you can try the whole thing without an MCP client:
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 9f2c1abAdd --json to any command for machine-readable output, and --repo/-r to
point at a repository other than the working directory.
gitlore serve --repo /path/to/repoFor Claude Code:
claude mcp add gitlore -- gitlore serve --repo /path/to/repoOr, in a client that reads a JSON config:
{
"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.
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.
uv venv && uv pip install -e ".[dev]"
pytest # fixtures build real git repos; nothing about git is mocked
ruff check .
mypyTests 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 covers the architecture and the reasoning.
MIT — see LICENSE.