Skip to content

Repository files navigation

GitBlinder horse-head logo wearing a blinder

GitBlinder

Give each coding agent only the part of GitHub it is responsible for.
A local MCP gateway that binds one agent connection to one repository, one branch, and one enforced responsibility.

Early alpha · local-first · open source

A racehorse wearing blinders leads a pack of horses without them

Prompts tell agents to stay in their lane. GitBlinder removes the other lanes.

GitBlinder keeps your GitHub token outside the agent and exposes a separate, enforced MCP perimeter for each responsibility. An agent connected to one perimeter receives only its allowed tools and path-filtered content. The repository and branch are fixed by the connection itself, not chosen by the agent or supplied in tool arguments.

Out-of-scope reads look absent. Out-of-scope writes never reach GitHub. Branch discovery and branch switching are kept outside the agent surface.

Why I built this

I started GitBlinder after launching a crowd of coding agents on a project built with spec-driven development (SDD).

At first, parallelism felt like a superpower. Then the repository and the agents' contexts grew:

  • agents became increasingly amnesiac as their context filled up;
  • they stepped on each other and overwrote work owned by another role;
  • implementation agents changed tests, or made them green without a real implementation behind them;
  • every new agent received far more repository context and GitHub authority than its task required.

The problem was not only prompting. Every agent could still see and touch everything. Their responsibilities were conventions, not boundaries.

GitBlinder applies the same idea as the single-responsibility principle to agent access: define one responsibility, carve out the smallest useful part of the repository, and bind one agent connection to it.

The agent becomes a narrow specialist. It cannot wander into another domain because that domain does not exist in its GitHub view.

The hypothesis

GitBlinder is an experiment in structurally focused agents. Enforced, disjoint perimeters should make it possible to:

Goal Mechanism
Preserve focus Fix the repository and branch at connection time, then hide irrelevant files and tools
Prevent collisions Give concurrent agents non-overlapping write perimeters
Protect test integrity Let an implementer read tests without being able to rewrite them
Build specialization Keep one agent inside one stable domain of the codebase
Reduce latency and cost Return less repository data and spend fewer tokens rediscovering context

These are hypotheses to measure, not benchmark claims. The companion gitblinder-sdd-lab provides a small, reproducible workload for comparing prompt-only roles with enforced ones.

Without GitBlinder every agent shares the full repository; with GitBlinder each agent is isolated to one folder

A concrete SDD workflow

Imagine one repository with a specification, its tests, and its implementation:

specs/**  ──►  test/**  ──►  src/**
   ▲              ▲             ▲
 reviewer      test author   implementer
 read-only     writes tests   writes source

The corresponding perimeters can be declared in YAML:

perimeters:
  spec-reviewer:
    repo: Math1987/gitblinder-sdd-lab
    tools: [get_tree, get_file, get_files, list_commits, get_commit]
    read:
      - "specs/**"
    write: []

  test-author:
    repo: Math1987/gitblinder-sdd-lab
    tools: [get_tree, get_file, get_files, push_files, list_commits, get_commit]
    read:
      - "specs/**"
      - "test/**"
      - "package.json"
    write:
      - "test/**"

  implementer:
    repo: Math1987/gitblinder-sdd-lab
    tools: [get_tree, get_file, get_files, push_files, list_commits, get_commit]
    read:
      - "specs/**"
      - "test/**"
      - "src/**"
      - "package.json"
    write:
      - "src/**"

At startup, each perimeter is bound to the repository's configured default branch (main when omitted). This creates three distinct MCP views:

http://127.0.0.1:7400/mcp/spec-reviewer
http://127.0.0.1:7400/mcp/test-author
http://127.0.0.1:7400/mcp/implementer

The test author cannot inspect the implementation. The implementer can read the fixed tests but cannot weaken them. With non-overlapping write globs, the two writing agents cannot overwrite one another. If an agent needs a feature branch, the operator allowlists it and the orchestrator connects the agent to /mcp/<perimeter>/<branch>.

How a perimeter works

GitHub token ─► GitBlinder
                  ├─ /mcp/spec-reviewer
                  │    └─ repo fixed · main fixed · specs/** read-only
                  ├─ /mcp/test-author/test-contract
                  │    └─ repo fixed · test-contract fixed · test/** writable
                  └─ /mcp/implementer/feature-parser
                       └─ repo fixed · feature-parser fixed · src/** writable
                                                        │
                                                        └─► GitHub

One local GitBlinder process owns the credential. Each named MCP endpoint is bound to one perimeter and revalidates every call before it reaches GitHub.

A perimeter controls:

  • repository — the agent cannot redirect a call to another repository;
  • bound branch/mcp/<id> fixes the repository default branch, while /mcp/<id>/<branch> fixes an operator-allowlisted branch;
  • tools — tools outside its allowlist do not appear in tools/list;
  • read paths — trees, files, and results are filtered through read globs;
  • write paths — every path in a mutation must match write, or the entire mutation is rejected;
  • commit history — commit lists use prefixes derived from read, and commit details expose only readable files plus an opaque omitted count.

read and write are independent. Reading a file does not imply permission to change it, and writing a path does not automatically reveal its current contents.

Design principles

Absence for reads, explicit refusal for writes

An out-of-scope file or branch looks like it does not exist. This avoids turning the gateway into an oracle for hidden repository structure.

An out-of-scope write returns a clear error, so the agent does not continue under the false belief that a change succeeded.

All-or-nothing mutations

push_files creates an atomic multi-file commit. If one requested path falls outside the write perimeter, nothing is forwarded to GitHub.

One connection, one responsibility

An agent does not select or switch its repository, perimeter, or branch during a tool call. Its MCP URL establishes the binding, and GitBlinder removes the bound owner, repo, ref, and branch fields from the relevant agent tool schemas.

The operator owns branches

New perimeters allow only the repository's default branch. The operator may discover GitHub branches and add an existing branch to a perimeter. The agent never receives list_branches or compare, even if either tool is mistakenly included in the perimeter's tool grant.

Opaque history filtering

list_commits is restricted to static path prefixes derived from read globs. get_commit filters its changed files through the full read perimeter. A mixed commit reports omitted_count without naming hidden paths; a purely out-of-scope commit looks absent.

Explicit ownership

GitBlinder does not infer architecture or automatically resolve overlapping responsibilities. To isolate concurrent agents, define deliberate, non-overlapping write globs that follow the boundaries of your system.

Quick start

Requirements

  • Node.js 20 or newer
  • A GitHub personal access token

Use a dedicated, least-privilege token. The token remains the maximum authority of the GitBlinder process; perimeters can only narrow it.

Install from npm

Install the compiled CLI globally:

npm install --global git-blinder
git-blinder --version

Or try it without a global install:

npx --yes git-blinder@latest --help

Install from source

npm install
npm run build
npm link

Start the local gateway. The token stays in the operator process and is never passed to an agent:

export GITHUB_TOKEN=github_pat_xxxxx
git-blinder serve
Authenticated as your-github-user
git-blinder is running on http://127.0.0.1:7400

In another terminal, inspect what the token can access and create a perimeter:

# Operator commands authenticate with the same token as the local gateway.
export GITHUB_TOKEN=github_pat_xxxxx

git-blinder repos list
git-blinder branches list --repo YOUR_GITHUB_USER/YOUR_REPO

git-blinder perimeter create implementer \
  --repo YOUR_GITHUB_USER/YOUR_REPO \
  --read 'specs/**' \
  --write 'src/**' \
  --tools get_tree,get_file,get_files,push_files,list_commits,get_commit

git-blinder perimeter show implementer
git-blinder perimeter branch list implementer

A newly created perimeter permits only the repository's default branch. To use another branch, create it on GitHub first, then explicitly add it:

git-blinder perimeter branch add implementer feature-parser

The orchestrator chooses the branch by choosing the MCP URL:

MCP URL Bound branch
/mcp/implementer Repository default branch
/mcp/implementer/feature-parser Allowlisted feature-parser branch

Connect an agent to a scoped endpoint, never /mcp/root:

claude mcp add git-blinder-implementer \
  --transport streamable-http \
  http://127.0.0.1:7400/mcp/implementer/feature-parser

Cursor configuration:

{
  "mcpServers": {
    "git-blinder-implementer": {
      "url": "http://127.0.0.1:7400/mcp/implementer/feature-parser"
    }
  }
}

Ops note: unfiltered tools live at /mcp/root (not for agents). /mcp is health-only. /api/* requires Authorization: Bearer with the same token used for serve. Agents connect only to /mcp/<id> or /mcp/<id>/<branch>.

The operator can inspect the same filtered history without giving the agent a branch-discovery tool:

git-blinder perimeter commits list implementer --branch feature-parser
git-blinder perimeter commits show implementer COMMIT_SHA --branch feature-parser

git-blinder perimeter list
git-blinder perimeter delete implementer

To preload stable perimeters at startup, copy and edit the YAML example:

cp config.example.yaml config.yaml
GITHUB_TOKEN=github_pat_xxxxx git-blinder serve --config config.yaml

Runtime changes currently live in memory. Put long-lived perimeter definitions in YAML until registry persistence is implemented.

Tool surface

/mcp/root contains the complete operator/debug catalogue. A perimeter endpoint intersects its optional tool grant with the safe agent catalogue. POST /mcp does not expose tools (405).

Tool Ops /mcp/root Perimeter endpoint
get_tree yes yes, path-filtered
get_file yes yes, path-filtered
get_files yes yes, path-filtered
push_files yes yes, atomic and write-filtered
list_commits yes yes, branch-bound and path-filtered
get_commit yes yes, readable files only
list_branches yes no
compare yes no

If tools is omitted from a perimeter, all safe perimeter tools are available. If it is present, only the granted safe tools appear in tools/list.

What works today

GitBlinder is an early alpha under active development. The current implementation includes:

  • local GitHub token validation and a streamable HTTP MCP server;
  • ops unfiltered MCP at /mcp/root ; health-only /mcp ; Bearer on /api/*;
  • YAML-preloaded and runtime-managed in-memory perimeters;
  • default-branch /mcp/<id> bindings and allowlisted /mcp/<id>/<branch> bindings;
  • forced repository and branch arguments, optional tool subsets, and path-scoped read / write;
  • strict path normalization and all-or-nothing writes via push_files;
  • operator CLI for repos/branches discovery and perimeter branch allowlists;
  • filtered commit history (list_commits via derived GitHub path= prefixes, get_commit with filtered files and omitted_count);
  • agent perimeter surface without list_branches / compare.

Current limitations worth knowing:

  • runtime perimeter and branch-allowlist changes are not persisted;
  • one process owns one GitHub token, and each perimeter targets one repository;
  • list_commits returns an empty list when no static prefix can be derived from a read glob such as ** or *.md;
  • /mcp/root remains available for operator/debug use (not for agents);
  • /api/* is protected by the ops Bearer token, but perimeter MCP URLs are still capability-by-path (guessable ids) in this version.

See SPEC.md for the model, CLI-SPEC.md for the CLI, and features/cli/ for executable contracts.

Security boundary

GitBlinder is a GitHub capability gateway, not an operating-system sandbox. Its guarantees assume that an agent:

  • reaches GitHub only through its scoped /mcp/<id>[/<branch>] endpoint;
  • cannot read the GitHub token from the environment or process;
  • does not also receive an unrestricted local clone, shell, or alternative GitHub credential that bypasses the gateway.

Bind the server to localhost, use a least-privilege upstream token, and give each agent only its named MCP endpoint.

Development

# Run from source
npx tsx src/cli/index.ts serve --config config.yaml

# Unit, integration, and E2E tests
npm test

# All supported behavioral contracts
npm run test:features

# Two remaining Git-write harness scenarios
npm run test:features:wip

# One feature file
npm run test:features -- features/cli/03_perimeter_mcp.feature

The test suite uses a deterministic mock GitHub upstream; no real repository is needed for perimeter and filtering tests. CLI and agent-perception contracts are wired and executable. Two scenarios remain explicitly tagged @wip while the mock Git write flow is completed: a successful bound-branch write and the cross-perimeter check after a concurrent write.

Documentation

License

MIT

About

No description, website, or topics provided.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages