Skip to main content
Glama

Secure Git MCP

A standalone derivative of the MCP Git reference server that enforces project path policy before returning Git output or changing repository state. All 12 upstream tools remain available. Restricted content produces an explicit policy notice; denied writes are rejected as a whole.

See upstream provenance, the approved design, and the preserved package license and upstream licensing notice.

Quick start with Docker

Build locally (the image has not been published):

docker build -t secure-git-mcp:local .

Copy examples/docker.mcp.json to your MCP client's .mcp.json, changing the absolute project path and numeric UID:GID. A complete configuration is:

{
  "mcpServers": {
    "secure-git": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i", "--network=none",
        "--user", "1000:1000",
        "--env", "SECURE_GIT_POLICY",
        "--mount", "type=bind,src=/absolute/path/to/project,dst=/repo",
        "secure-git-mcp:local", "--repository", "/repo"
      ],
      "env": {
        "SECURE_GIT_POLICY": "{\"deny\":[\"^private(/|$)\",\"^modules/payments(/|$)\"]}"
      }
    }
  }
}

The client's env entry sets the Docker process environment; --env SECURE_GIT_POLICY forwards it into the container. The server's tool calls use repo_path: "/repo", not the host path. Use -i without -t: MCP runs over stdio, not an HTTP port. Runtime networking is disabled; local Git operations do not need it. Network access is needed while building dependencies.

The image defaults to UID:GID 1000:1000. Set --user to the repository owner's output from id -u and id -g on Linux, and ensure the working tree and Git directory are readable/writable by that identity. Git commits require a locally configured author name and email. Do not bypass Git ownership checks with safe.directory=*; fix ownership or explicitly trust the one intended repository in operator-controlled Git configuration. Linked worktrees also need their external Git metadata mounted at the correct location; a normal standalone clone is easiest to deploy. A read-only bind mount permits reads but cannot support write tools.

To keep policy in an external trusted .mcp.json instead of forwarding env, mount it read-only at /config/.mcp.json and append --mcp-config /config/.mcp.json after the image name. This file has the same mcpServers.secure-git.env.SECURE_GIT_POLICY structure shown above. Mount only the intended working tree and required trusted configuration.

Related MCP server: scopeguard-mcp

Native development and use

Requires Git, uv, and Python >=3.10. .python-version pins the upstream development interpreter to Python 3.10; Docker uses Python 3.12.

uv sync --frozen --dev
SECURE_GIT_POLICY='{"deny":["^private(/|$)","^modules/payments(/|$)"]}' \
  uv run --frozen secure-git-mcp --repository /absolute/path/to/project

For an MCP client, use examples/native.mcp.json. It runs this checkout with uv --directory /absolute/path/to/secure-git-mcp run --frozen secure-git-mcp .... Installing the upstream package from PyPI is not an installation of this derivative: use this checkout or its built wheel.

The compatibility command mcp-server-git and python -m mcp_server_git both invoke the same secured CLI. --repository is mandatory and pins one repository; a tool cannot open a different or nested repository. When the startup path points inside a repository, it resolves to its working-tree root. Tool repo_path must identify that root (a real filesystem alias is accepted).

Optional arguments:

uv run --frozen secure-git-mcp \
  --repository /absolute/path/to/project \
  --mcp-config /trusted/location/.mcp.json \
  --server-name secure-git

--mcp-config explicitly selects policy from that file instead of process env; an invalid selected file never falls back to another source. --server-name defaults to secure-git. Missing/malformed policy prevents startup. An explicit {"deny":[]} allows ordinary paths, while metadata/path-safety restrictions remain enforced. Policy is loaded once per server startup.

Regex policy

SECURE_GIT_POLICY is a JSON object with exactly one key, deny, containing regex strings. Within .mcp.json this JSON object is encoded as an environment-variable string.

Regex

Meaning

`^private(/

$)`

`^modules/(payments

billing)(/

`(^

/)secrets(/

\\.pem$ in JSON source

Deny filenames ending in .pem (regex \.pem$)

Matching uses case-sensitive regex search, applied to each normalized POSIX repository-relative path and all its ancestor directories. Thus ^private$ also denies private/a/b.txt; private without anchors may also match notprivate. Modules are paths, not language-aware import dependencies. Use / on every platform. Invalid regexes, unknown policy keys, traversal, absolute paths, NULs, and .git components fail closed. Git pathspec magic is never accepted as pattern syntax for git_add: filenames are literal.

Keep policy and launcher configuration under operator control, outside the agent's writable scope. Configure any other shell, filesystem, or MCP tools separately: this server cannot restrict those tools or stop them modifying a policy file and restarting a process.

Inherit Claude project deny paths

The server automatically reads <repository>/.claude/settings.json at startup and imports sandbox.filesystem.denyRead. Additional MCP deny regexes still apply: a match in either source denies access, including writes and historical Git reads.

{
  "sandbox": {
    "filesystem": {
      "denyRead": ["private", "modules/payments/**", "**/.env", "**/*.pem"]
    }
  }
}

If the Claude file is absent, the server falls back to MCP rules. Missing sandbox, filesystem, or denyRead sections mean no inherited denies. An existing malformed/unreadable file or dangling settings symlink prevents startup. Explicit MCP policy remains required; use {"deny":[]} to add no extra regex restrictions. Policy is frozen until restart, including the missing-file fallback.

Inherited entries are paths/globs, never regexes: * stays within one path component, ** crosses directories, ? matches one non-separator, and bracket sets are supported. A directory match denies its descendants. Matching includes dotfiles and historical/absent paths; it does not expand only files currently on disk. Literal spellings are also denied, and existing literal symlink prefixes are resolved to cover target aliases. Unsupported backslash patterns, unresolved home names, globs ending in /, and .. after wildcard components fail closed. Relative paths resolve from the served repository root; absolute paths and ~ use the server's filesystem/account. Prefer relative paths in Docker: host absolute paths do not automatically map to /repo.

This imports only the project's deny list, not the complete platform sandbox policy: allowRead exceptions cannot remove these denials, and user-level settings and settings.local.json are not imported. Keep the project settings file operator-controlled. With Docker, include .claude/settings.json in the repository mount; an omitted file intentionally activates MCP-only fallback.

Tool behavior

Tool

Enforcement

git_status

Structured per-file records; omit protected paths and either side of protected renames. No raw branch/upstream summaries.

git_diff_unstaged, git_diff_staged, git_diff

Check old and new paths, then render complete permitted file changes. External diff, textconv, and submodule expansion are disabled.

git_show

Accept commits and commit:path. Filter commit patches and directory listings. Deny protected paths, raw blob/tree IDs, index-stage expressions, and symlink/gitlink content.

git_log

Omit hidden-only commits; withhold messages/authors for mixed commits. Permitted commit metadata remains visible.

git_branch

List local/remote/all branch names, with optional commit filters. Names are not secret-scanned.

git_add

Expand literal files/directories or .; refuse the whole selection if any affected path or resolved symlink target is restricted. Embedded repositories and gitlinks are refused before staging.

git_commit

Refuse a staged set containing any restricted path, symlink, gitlink, or conflict. Initial commits are supported.

git_reset

Refuse when restricted entries are staged, including intent-to-add entries. Otherwise unstage everything, preserving working files.

git_create_branch

Validate branch/base and create the ref without changing files.

git_checkout

Refuse changes involving restricted tree paths, symlinks, gitlinks, or restricted staged changes. No forced checkout, overwriting ignored files, hooks, or recursive submodule updates.

Write operations refuse in-progress merge, rebase, cherry-pick, revert, autostash, or sequencer state. All calls are serialized within the process; this cannot lock out external Git processes. Hooks are disabled for server-initiated commands. Git command failures and argument-validation failures return safe generic errors rather than stderr or echoed argument values.

Partial results include “Some content is unavailable according to project policy.” Denied calls return isError: true with “Access denied according to project policy.” A generic error may also mean invalid arguments, an unavailable revision, a Git failure, or another blocked operation; raw Git diagnostics are deliberately withheld.

Path policy does not classify arbitrary copied text. Copies of protected content in permitted files, author names/messages on entirely permitted commits, and branch names are not automatically redacted. Similarity-based rename detection checks both paths; a copy or heavily rewritten file at an allowed path is governed by that allowed path. Do not treat directory rules as automatic secret detection or an OS sandbox. Use a trusted repository/Git configuration and exclusive access for writes. Git attribute filters and signing/credential helpers must be controlled by the operator; runtime networking is disabled in the Docker example.

Extending output filtering

Path policy operates on structured Git data first. Trusted server integrators can then compose output filters, including future token/key redactors:

from pathlib import Path
from mcp_server_git.filters import FilterContext
from mcp_server_git.policy import PathPolicy
from mcp_server_git.secure_server import create_server

class Redactor:
    def filter(self, text: str, context: FilterContext) -> str:
        return text.replace("EXAMPLE_TOKEN", "[REDACTED]")

server = create_server(
    Path("/repo"),
    PathPolicy.from_json('{"deny":["^private$"]}'),
    filters=[Redactor()],
)
# Run with: await server.run_stdio_async()

See examples/redactor.py for a regex example. Filters run in order on successful tool results and anticipated tool errors, with tool_name and is_error context. If any filter fails or returns a non-string, the entire output is withheld. The default CLI performs path filtering only; it does not automatically load custom code from .mcp.json. Automatic token/key detection is a future extension, not a feature claimed by this version.

Verification

uv sync --frozen --dev
uv run --frozen pytest -q
uv run --frozen ruff check .
uv run --frozen ruff format --check .
uv run --frozen pyright --pythonpath .venv/bin/python
uv build
uv run --frozen python scripts/smoke.py
docker build -t secure-git-mcp:local .
uv run --frozen python scripts/smoke.py --docker secure-git-mcp:local

The smoke check creates a disposable repository, makes real MCP requests over stdio, verifies permitted/denied reads, stages and commits permitted files, and checks that denied staging leaves the index unchanged. Docker verification uses a non-root UID/GID and --network=none. If your Docker daemon can only bind directories under a shared path, use scripts/smoke.py --docker secure-git-mcp:local --parent /path/shared/with/daemon.

If your home is read-only (as in this cloud environment), set writable tool caches first:

export UV_CACHE_DIR=/workspace/.cache/uv
export UV_PYTHON_INSTALL_DIR=/workspace/.cache/uv-python
export DOCKER_CONFIG=/workspace/.cache/docker

In this cloud, Docker builds also need the proxy hostname mapping and public CA bundle. Use uv run --frozen python scripts/build_cloud_image.py to pass them through supported Docker build arguments and a temporary BuildKit CA mount. No proxy credential or cloud CA is baked into the image; TLS and package-signature checks remain enabled. This build helper uses host networking for installation only; runtime smoke checks still disable networking.

Upstream reference helpers remain in server.py to preserve backend/protocol regression coverage. The public CLI and secure_server.py are the enforced interface; integrators should use secure_server.create_server, not instantiate the historical upstream reference server. No image/repository is pushed automatically.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    B
    maintenance
    Enables policy-first defensive security operations for MCP, providing repository and web-security analysis with controlled authorization, scoped execution, and auditability.
    9
    1
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables safe filesystem and git operations confined within a single allowed directory tree, using symlink-resolved path containment to protect against path traversal. Users can read, write, list, and search files, as well as initialize repos and run status, stage, commit, diff, and log operations through MCP.
    11
    MIT
  • F
    license
    B
    quality
    C
    maintenance
    Enables secure, policy-controlled operations on local git repositories through MCP, including bounded reads, context-checked patches, allowlisted tasks, and guarded git operations.
    10
    -