DO NOT open a public GitHub issue for security vulnerabilities. Instead, please report them responsibly to:
Email: security@rethunk.tech
Response SLA: We aim to respond to security reports within 24 hours.
When reporting a vulnerability, please include:
- Description of the vulnerability
- Affected component(s) and version(s)
- Steps to reproduce (if applicable)
- Potential impact
- Suggested fix (optional)
mcp-multi-root-git is an MCP stdio server that exposes 25 git tools to LLMs. It runs with the OS user's permissions and can read and modify local repositories the operator (or client) can reach.
Fan-out read (root: one path, path list, or "*"):
| Tool | Risk note |
|---|---|
git_status |
Working-tree / branch metadata |
git_inventory |
Status + ahead/behind across roots |
git_parity |
HEAD equality across path pairs |
list_presets |
Preset file discovery |
git_log |
Commit history / subjects / shortstat |
git_grep |
Pickaxe history search (commit SHAs + subjects) |
Single-repo read (workspaceRoot):
| Tool | Risk note |
|---|---|
git_diff_summary |
Structured diffs (truncated) |
git_diff |
Raw scoped diff text |
git_show |
Commit message + diff, or file content at a ref |
git_conflicts |
Conflict hunk text (ours/theirs/base) |
git_blame |
Authorship + line content runs |
Mutating (workspaceRoot only — no multi-repo fan-out on writes):
| Tool | Effect |
|---|---|
batch_commit |
Stages listed paths / hunks and creates commits; optional push-after |
git_push |
Pushes current branch; never force-pushes |
git_merge |
Merges sources into a destination; optional cleanup skips protected names |
git_cherry_pick |
Applies commits onto a destination; same protected-name cleanup rules; onConflict: "pause" leaves conflict + sequencer state in place instead of aborting; refuses a second call while one is already in progress |
git_cherry_pick_continue |
Resumes (--continue) or rolls back (--abort) a cherry-pick left in progress; stateless, reads CHERRY_PICK_HEAD live |
git_reset_soft |
Moves branch tip (--soft); history rewrite of the tip (objects kept in index); requires ref to be an ancestor of HEAD unless force: true |
git_revert |
New inverse commit(s); does not rewrite history; onConflict: "pause" leaves conflict + sequencer state in place instead of aborting |
git_revert_continue |
Resumes (--continue) or rolls back (--abort) a revert left in progress; stateless, reads REVERT_HEAD live |
git_tag |
Create / delete tags (protected names refused in both roles) |
git_branch |
Create / delete / rename local branches (protected names refused) |
git_worktree_add |
Adds a linked worktree (protected branch names refused) |
git_worktree_remove |
Removes a linked worktree (not the main worktree) |
git_stash_apply |
Apply or pop a stash entry |
git_stash_push |
Creates a new stash (may clear working-tree changes) |
There is no git_pull (or git_fetch) tool — the server never touches remote refs except to publish via git_push. Optional batch_commit push: "after" uses the same non-force push path.
workspaceRoot (and fan-out root path strings) are trusted operator input. The server resolves a path to a git toplevel and operates there. It does not whitelist those paths against the MCP session's advertised roots.
Consequence: any LLM tool call that supplies an absolute (or resolvable) path to a git repository the OS user can read/write can target that repository. Host clients must treat tool arguments as privileged and constrain who may invoke mutating tools.
MCP roots still matter for discovery and for root: "*" fan-out (session-advertised roots). They are not a sandbox boundary for explicit path arguments.
Once a git toplevel is selected, file-path arguments (commit paths, blame path, diff paths, stash pathspecs, etc.) are confined with realpath-based checks in src/repo-paths.ts (resolvePathForRepo / assertRelativePathUnderTop / isStrictlyUnderGitTop). Paths must resolve strictly under that toplevel; symlink escape outside the chosen repo is rejected.
This is per-repo path confinement, not an MCP-root allowlist. Choosing the wrong toplevel (see trust model above) is outside that control.
Exception — git_worktree_add / git_worktree_remove path is not repo-confined. By design (src/server/git-worktree-tool.ts), sibling worktrees outside the chosen git toplevel are an intentional, documented use case, so path is not run through resolvePathForRepo / assertRelativePathUnderTop. The only guards are argv-injection guards: empty/whitespace-only, NUL bytes, and leading-dash / option-like basenames are rejected (invalid_paths) so the path can't be parsed as a git flag; the resolved path can otherwise point anywhere on disk the OS user can write (any path, not just siblings of the repo). Blast radius: a caller can create or remove a linked worktree at an arbitrary filesystem location reachable by the OS user, not just under the selected repo.
Mutating tools can lose uncommitted work, move refs, create or delete branches/tags/worktrees/stashes, or publish commits. Shipped controls (not aspirational TODOs):
| Control | Behavior |
|---|---|
| No force-push | git_push has no --force / --force-with-lease mode for any branch |
| Soft reset only | git_reset_soft is soft; hard/mixed reset are not exposed |
| Non-rewriting revert | git_revert adds inverse commits; does not rewrite tip history |
| Protected branches | See below — enforced on git_branch, git_worktree_add, and merge/cherry-pick cleanup |
| Pathspecs on commit | batch_commit stages only the listed entry files / hunks |
| Dirty-tree gates | Merge, cherry-pick, revert, and soft-reset refuse unsafe dirty trees as documented per tool |
History rewrite vs non-rewrite:
- Rewrites tip (local):
git_reset_softmovesHEAD/branch tip; previously committed tip commits remain reachable via reflog until GC, but the branch no longer points at them. Force-push is unavailable, so remote history is not overwritten through this server. - Non-rewriting:
git_revert,batch_commit,git_merge,git_cherry_pick,git_tag, branch/worktree/stash tools (aside from deleting refs they own).
Exact names (case-insensitive, optional refs/heads/ prefix stripped): main, master, dev, develop, stable, trunk, prod, production, head.
Patterns: release-* / release/* and hotfix-* / hotfix/* (see PROTECTED_PATTERN in src/server/git-refs.ts).
Enforcement:
git_branchrefuses protected names in any role (create / delete / rename source or target)git_worktree_addrefuses protected branch namesgit_merge/git_cherry_pickoptional branch/worktree cleanup skips protected names
Protected-branch checks do not mean force-push is gated — force-push simply does not exist.
Git is invoked via argv arrays (spawnGitAsync in src/server/git.ts) — no shell, so shell metacharacter injection through a joined command string does not apply.
Untrusted tokens (refs, ranges, remotes, upstream names, etc.) must still pass validators such as isSafeGitRefToken, isSafeGitRangeToken, isSafeGitCommitIsh, isSafeGitAncestorRef, and isSafeGitUpstreamToken before being placed on the argv. Callers and future tools must keep routing user-controlled strings through those gates.
Git child processes no longer inherit the full ambient process.env. spawnGitAsync (and the remaining sync spawnSync probes in src/server/git.ts) build the child's env from a fixed allowlist — PATH, HOME, USER, LOGNAME, LANG, LC_*, TZ, TMPDIR, XDG_CONFIG_HOME, XDG_CACHE_HOME, SSH_AUTH_SOCK, SSH_AGENT_PID, GIT_SSH, GIT_SSH_COMMAND, GIT_ASKPASS, GIT_TERMINAL_PROMPT, GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM, GIT_CONFIG_NOSYSTEM — plus whatever the deployer opts into via RETHUNK_GIT_ENV_PASSTHROUGH.
Threat model: everything else ambient is dropped, including other GIT_* vars such as GIT_DIR, GIT_WORK_TREE, and GIT_INDEX_FILE. An attacker (or a misconfigured co-process sharing the same environment) that could previously set one of these on the server's ambient environment could redirect a tool call believed to target cwd at a different repo, index, or work tree — a confused-deputy vector. The allowlist closes that off by construction rather than by enumerating unsafe names. Explicit env vars our own code passes per-call (SpawnGitOpts.env — e.g. GIT_AUTHOR_* in tests) still merge on top of the filtered base, so legitimate internal use is unaffected. RETHUNK_GIT_ENV_PASSTHROUGH is an explicit, deployer-controlled escape hatch — widening it re-opens whatever names are listed, so only add names you trust the deployment's ambient environment to hold safely.
Read-only tools can surface secrets already present in tracked history or the working tree: git_show (file-at-ref), git_blame, git_grep, git_log, git_diff / git_diff_summary, and git_conflicts hunk text. Truncation reduces volume; it does not redact secrets. Treat any repo that may contain credentials as sensitive when exposing these tools to an LLM.
- Push and fetch use the host's git credential helpers / SSH agent; the server does not embed credentials
- Credentials must never be logged; this server does not implement a durable write-operation audit log (tool responses are the MCP client's responsibility to retain if needed)
- Prefer SSH agent or OS credential storage; do not put PATs in tool arguments or env visible to the model
Set RETHUNK_GIT_TOOLS to a comma-separated allowlist of tool names to shrink the registered surface (for example omit all mutators in a read-only deployment). When unset, all 25 tools register. Details: docs/install.md.
- Constrain which principals may call mutating tools; remember
workspaceRootis trusted - Use
RETHUNK_GIT_TOOLSto drop write tools where not needed - Keep git CLI, Node/Bun runtime, and dependencies (
fastmcp,zod, etc.) patched; runbun auditregularly - Prefer dedicated non-production clones when evaluating mutators
- Do not store secrets in tracked files that read tools can return
Latest release only.
| Version | Supported |
|---|---|
| Latest | ✅ Yes |
None currently known. Reports are welcome via security@rethunk.tech.
- Exercise path confinement with symlink escape attempts under a chosen git toplevel
- Confirm
git_pushrejects any attempt to introduce force flags (none are accepted in the schema) - Confirm protected-name refusals on
git_branch/git_worktree_addand cleanup skips on merge/cherry-pick - Confirm invalid ref tokens fail validators before spawn
- Run mutators only on disposable test repositories
If a security vulnerability is discovered:
- Report immediately to security@rethunk.tech (do not disclose publicly)
- Include reproduction steps and affected version(s)
- Allow 24-48 hours for initial response and triage
- Coordinate disclosure timeline if a patch is required
- Credit will be given to the reporter (if desired)
- Security Issues: security@rethunk.tech
- General Support: support@rethunk.tech
- Website: https://rethunk.tech
Last updated: 2026-07-18