Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gitlore

CI Python License: MIT

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

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:

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

gitlore serve --repo /path/to/repo

For Claude Code:

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

Or, 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.

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

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 covers the architecture and the reasoning.

License

MIT — see LICENSE.

About

Read-only, structured access to a Git repository's history — as an MCP server and a CLI. Hardened against option injection and hostile repo config.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages