Local MCP server for a narrow, policy-constrained set of Git and GitHub operations that Codex can call with fewer repeated shell and sandbox approval prompts.
It exists to handle a small approved workflow through MCP tools: inspect repository state, stage and commit changes, fetch, pull, and push the current branch, create or switch local branches in a constrained way, and open draft pull requests.
This is especially useful with OpenAI Codex sandbox, where protected-path behavior still applies to paths such as .git. In practice, shell Git operations that write repository metadata can still be blocked or require approval, while direct GitHub network mutations may still be allowed or approval-gated depending on runtime policy.
The MCP tool metadata in this server is explicit about which tools are read-only, which ones are additive mutations, and which ones may replace existing local or remote state. That can help Codex make better approval decisions, but it does not bypass Codex approval policy or higher-level safety monitoring. Repository mutations may still prompt for approval or be cancelled by the client/runtime.
See OpenAI Codex docs: Protected paths in writable roots.
For the suggested repository workflow used in this repo (and in general), see AGENTS.md.
Current tool surface:
config_bootstrapconfig_upsert_repogit_repo_policygit_statusgit_addgit_stagegit_commitgit_fetchgit_sync_basegit_pull_current_branchgit_worktree_addgit_pushgit_branch_create_and_switchgit_branch_switchgh_pr_create_draft
The server is intentionally not a generic git or gh proxy. Inputs are structured, commands are fixed, and unsupported operations are denied by default.
MIT. See LICENSE.
- Node.js
- npm
git2.23 or newergh
git_branch_switch uses git switch rather than git checkout, so Git 2.23+ is the supported baseline for branch switching.
From this repository:
npm installUse a config file at ~/.config/codex-git-unleash-mcp.yaml.
If that file does not exist yet, the server still starts and exposes the full tool surface. Global config is optional at runtime: repositories can also be authorized through a fixed repo-local policy file at .git-unleash.yaml.
config_bootstrap and config_upsert_repo can still create or update the YAML when you want an external allowlist.
config_bootstrap and config_upsert_repo are administrative conveniences, not security controls. A caller that can invoke them can create or widen repository policy, and runtime tools will use the changed policy on their next call. Hosts should expose these tools only to trusted callers; otherwise, omit them from the available tool set or require host-side approval for each invocation.
Example:
defaults:
allowed_branch_patterns:
- "^main$"
feature_branch_pattern: "<user>/<feature-name>"
git_worktree_base_path: /tmp/git-worktrees
allow_draft_prs: true
workflow_mode: worktree
allowed_workflow_modes:
- feature_branch
- worktree
repositories:
- path: ~/projects/codex-git-unleash-mcp
default_remote: origin
- path: ~/projects/another-repo
allowed_branch_patterns:
- "^feature/[a-z0-9._-]+$"
feature_branch_pattern: "feature/<feature-name>"
allow_draft_prs: false
workflow_mode: feature_branch
allowed_workflow_modes:
- feature_branchNotes:
pathmust be an absolute path or start with~/config_bootstrapcreates a minimal valid YAML config file and refuses to overwrite an existing fileconfig_upsert_repoadds or updates one repository entry in the YAML config and matches existing entries by canonical repository path- config changes are reloaded from disk on the next tool call, so a server restart is not required after
config_bootstraporconfig_upsert_repo - top-level
defaultsare optional and may defineallowed_branch_patterns,feature_branch_pattern,git_worktree_base_path,default_remote,allow_draft_prs,workflow_mode, andallowed_workflow_modes - repository values override top-level defaults field-by-field
feature_branch_patternis an optional suggested naming template for new feature branches; it is advisory metadata and does not grant permission to use a branch name that failsallowed_branch_patternsallowed_branch_patternsandfeature_branch_patternsupport a dedicated<user>placeholder, resolved fromUSER, thenUSERNAME, then the system account username; other environment-variable expansion is intentionally not supportedgit_worktree_base_pathis inherited or overridden per repository and, when configured, constrainsgit_worktree_add.pathto stay under that base- for Codex workflows, prefer a repo-specific in-repository worktree base such as
.worktrees/when you want linked worktrees to stay under the same trusted project root; add that directory to.gitignore workflow_modeis optional preference metadata for agents; it tells them which Git setup path to try first for this repository. Supported values areworktree,feature_branch, andcurrent_branchallowed_workflow_modesis the explicit authorization boundary for setup tools and acceptsworktree,feature_branch, orcurrent_branch- when both fields are present, agents should treat
workflow_modeas the preferred starting flow andallowed_workflow_modesas the list of setup flows they may actually use - when
allowed_workflow_modesis omitted butworkflow_modeis set, setup tools derive the allowed mode fromworkflow_modefor backward compatibility - when both
allowed_workflow_modesandworkflow_modeare omitted, setup tools fail closed and ask the caller to inspectgit_repo_policy current_branchcannot be combined withfeature_branchorworktreeinallowed_workflow_modes- when both fields are set,
workflow_modemust be included inallowed_workflow_modes worktreemeansgit_worktree_addis authorizedfeature_branchmeansgit_branch_create_and_switchandgit_branch_switchare authorizedcurrent_branchmeans do not create a new worktree or feature branch; work directly on the current allowed branch- branch patterns are full-match regexes against the current branch name
- keep branch patterns simple: advanced group syntax, backreferences, and nested quantifiers are rejected at config load time
- each repository must end up with at least one effective allowed branch pattern, either from the repo entry or inherited
defaults git_repo_policyreturns the configured branch patterns and related repository defaults for an authorized repository, includingfeature_branch_pattern,git_worktree_base_path, the preferredworkflow_mode,allowed_workflow_modes, the policy source, whether repo overrides were applied, and the repo-local config path when applicablegit_add,git_stage,git_commit,git_sync_base,git_pull_current_branch,git_push, andgh_pr_create_draftrequire the current branch to match one of the configured patterns- repository-mutating Git and GitHub tools are serialized per worktree inside the MCP process, and allowed-branch mutations re-check the current branch before returning success
- that process-local lock does not stop a different process from changing the worktree concurrently, but branch drift is reported as an error instead of being reported as a clean success
git_fetchonly requires the repository to be authorized, fetches from the resolved remote, updates local fetch metadata and remote-tracking state only, and uses an explicit branch when provided or the detected base branch otherwisegit_sync_baserequires a clean worktree, fetches the detected remote base branch, merges only that remote-tracking ref into the current allowed branch, and aborts the merge before returning an error if a conflict occursgit_pull_current_branchrequires a clean worktree, fetches the current branch from the resolved remote, merges only that remote-tracking ref into the current allowed branch, and aborts the merge before returning an error if a conflict occursgit_worktree_addrequires an explicit absolute target path, validates the requested new branch name againstallowed_branch_patterns, creates a linked worktree from an explicit or detected upstream base branch, and requiresworktreein the effective allowed workflow modes- when
git_worktree_base_pathis configured,git_worktree_add.pathmust resolve under that base path git_branch_create_and_switchandgit_branch_switchrequire a clean worktreegit_branch_create_and_switchalso requires the requested new branch name to matchallowed_branch_patterns, and requiresfeature_branchin the effective allowed workflow modesgit_branch_switchalso requires the requested branch name to matchallowed_branch_patterns, and requiresfeature_branchin the effective allowed workflow modes- remote resolution prefers configured
default_remotewhen present and valid, then the current branch's remote, thenorigin - branch creation and PR base resolution prefer the remote HEAD branch and fall back to GitHub default-branch detection when needed
git_statusonly requires the repository to be authorized
Policy precedence:
- if
.git-unleash.yamlexists, it is the base policy for that repository - if the global config has a matching repository entry, that repository entry overrides the repo-local base field-by-field
- otherwise, top-level
defaultsare the base policy - if the global config has a matching repository entry, that repository entry overrides the defaults base field-by-field
- for backward compatibility, an explicit
workflow_modewith noallowed_workflow_modesalso sets the effective allowed workflow modes to that single workflow
Repositories can opt into zero-setup authorization with a fixed repo-local file at .git-unleash.yaml in the repository root.
Example:
allowed_branch_patterns:
- "^<user>/.*$"
feature_branch_pattern: "<user>/<feature-name>"
git_worktree_base_path: .worktrees
workflow_mode: worktree
allowed_workflow_modes:
- feature_branch
- worktreeRepo-local policy rules:
.git-unleash.yamlreplaces top-leveldefaultswhen it exists- a matching repository entry in the global config may still override repo-local values field-by-field
- global config still works as a fallback when a repository does not define
.git-unleash.yaml - for repo-local policy,
git_worktree_base_pathmay be relative to the repository root - repo-local policy must not set
default_remote - runtime tools compare the repo-local policy in the local remote-tracking base ref, index, and working tree, and fail closed on any divergence
- when the trusted base copy is missing locally, runtime tools fetch that base branch and retry the trust check before failing closed
- in a fork, the fork's own base branch is authoritative for repo-local policy
- this prevents locally widened repo-local policy from being used for MCP operations
Example opt-in override from a matching global repository entry:
Global config:
defaults:
feature_branch_pattern: "defaults-do-not-win/<feature-name>"
repositories:
- path: /Users/alice/project
feature_branch_pattern: "bob/<feature-name>"Repo-local .git-unleash.yaml:
allowed_branch_patterns:
- "^[a-zA-Z0-9.]/.*$"
feature_branch_pattern: "<user>/<feature-name>"Effective policy:
allowed_branch_patterns:
- "^[a-zA-Z0-9.]/.*$"
feature_branch_pattern: "bob/<feature-name>"In that example, the matching repository entry overrides feature_branch_pattern, while the global defaults.feature_branch_pattern is ignored because repo-local policy replaces defaults for that repository.
The intended happy path is:
- If the config file does not exist yet, call
config_bootstrapto create it. - Call
config_upsert_repoto add or update an allowlisted repository entry when needed. - Or check in
.git-unleash.yamlto authorize the repository through repo-local policy instead of the global YAML. - Call
git_repo_policyorgit_statusto inspect the authorized repository. - Prefer the setup flow advertised by
git_repo_policy.workflow_modeas the way to start work in that repository. - Before creating a branch, use
git_repo_policyto confirm the configuredallowed_branch_patterns, then choose a new branch name that matches that policy. - Call
git_branch_create_and_switchwhen the preferred or selected setup flow isfeature_branchand you need a new local branch in the current worktree. - Call
git_worktree_addwhen the preferred or selected setup flow isworktreeand you need a separate linked worktree on a new allowed branch at an explicit absolute path. - Call
git_sync_basewhen you need to bring the detected remote base branch into the current allowed branch without exposing generic merge controls. - Call
git_pull_current_branchwhen you need to bring the current branch's resolved remote branch into the current allowed branch without exposing generic merge controls. - Call
git_addorgit_stagewith explicit repository-relative paths. - Call
git_commitwith a normal commit message. - Call
git_pushto push the current branch to the resolved remote. - Call
gh_pr_create_draftto open a draft PR against an explicit base or the detected default base branch.
Each step stays inside a fixed policy boundary. There is no arbitrary checkout, no arbitrary push refspec, no amend flow, and no non-draft PR creation.
Start the server over stdio with the config path as the first argument:
npm run dev -- ~/.config/codex-git-unleash-mcp.yamlYou can also provide the config path through GIT_UNLEASH_MCP_CONFIG:
GIT_UNLEASH_MCP_CONFIG=~/.config/codex-git-unleash-mcp.yaml npm run devIf the config path points to a file that does not exist yet, the server still starts. Runtime tools can still operate on repositories that are authorized through .git-unleash.yaml; otherwise they behave as unauthorized until you create or update the global YAML.
npm run buildnpm run typecheck
npm testRegister the MCP server with the wrapper script:
codex mcp add git_unleash -- ~/projects/codex-git-unleash-mcp/scripts/run-mcp.sh ~/.config/codex-git-unleash-mcp.yamlThen verify:
codex mcp listThe wrapper is there so the MCP server can inherit or reconstruct the SSH agent socket when Git operations need it.
It also plays nicely with child-process PATH fallback inside the Node server. By default, command execution now preserves the inherited PATH and appends common system locations, including Homebrew paths on macOS such as /opt/homebrew/bin, so gh and similar tools are found more reliably even when the MCP host starts with a minimal environment.
This matters in two common cases:
git_commitwhen Git is configured for SSH-based commit signinggit_fetchandgit_pushwhen the repository remote uses SSH authentication
If your shell already has a working SSH_AUTH_SOCK, start Codex from that shell so the MCP server inherits it.
If Codex does not inherit the socket automatically, set one of these before starting Codex:
export GIT_UNLEASH_SSH_AUTH_SOCK="$SSH_AUTH_SOCK"or:
export SSH_AUTH_SOCK=/path/to/ssh-agent.sockOn macOS, the wrapper will also try launchctl getenv SSH_AUTH_SOCK before failing.
For GUI-launched Codex on macOS, a stable SSH agent socket is usually more reliable than the system SSH_AUTH_SOCK. The system socket can be dynamic, and launchctl setenv values do not survive a reboot. If you use an agent with a stable socket, configure the MCP server with that socket explicitly.
For example, 1Password's SSH agent exposes this socket on macOS:
/Users/<user>/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock
Add the socket to your Codex config:
[mcp_servers.git_unleash.env]
SSH_AUTH_SOCK = "/Users/<user>/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"
GIT_UNLEASH_SSH_AUTH_SOCK = "/Users/<user>/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"Then verify that the agent exposes the key Git is configured to use for SSH signing:
SSH_AUTH_SOCK="/Users/<user>/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" ssh-add -L
git config --get user.signingkeyuser.signingkey may be either a .pub file path or Git's inline key::ssh-ed25519 ... format. In both cases, the public key shown by ssh-add -L must match the public key configured by user.signingkey. The comments may differ, but the key type and key body must match.
If you use Apple's built-in SSH agent and Keychain integration, keep in mind that this SSH config:
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519does not necessarily load identities into the agent at login. It can be lazy and load the key only after an SSH operation runs. That means a preflight like ssh-add -L can still report no identities immediately after a reboot. In that setup, either load the key before starting Codex or use a stable agent socket such as 1Password, Secretive, or another dedicated SSH agent.
Troubleshooting checklist:
- If startup fails with
SSH_AUTH_SOCK is not set, the MCP host did not pass a socket and the wrapper could not recover one throughGIT_UNLEASH_SSH_AUTH_SOCKorlaunchctl getenv SSH_AUTH_SOCK. - If startup fails with
ssh-agent is reachable ... but it is not returning any identities, the socket is valid butssh-add -Lcannot see a loaded key. - If Terminal Git works but MCP startup fails after reboot, check whether Terminal lazily loaded the key after the MCP server had already started.
- If you use a stable agent socket, prefer setting both
SSH_AUTH_SOCKandGIT_UNLEASH_SSH_AUTH_SOCKin the MCP server config so the wrapper and child Git processes agree on the same agent.
If you need an exact PATH instead of the built-in fallback behavior, you can still register the server with an explicit environment override:
codex mcp add git_unleash --env PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" -- ~/projects/codex-git-unleash-mcp/scripts/run-mcp.sh ~/.config/codex-git-unleash-mcp.yamlOnce registered, Codex should be able to use:
config_bootstrapto create the initial YAML config file when it does not exist yet; it writes a minimal valid config and the runtime tool handlers will see it on their next callconfig_upsert_repoto add or update one repository entry in the YAML config; it validates the resulting file against the existing schema, matches existing repos by canonical path, and the runtime tool handlers will see the updated policy on their next callgit_repo_policyto inspect the configured path, canonical path, allowed branch patterns, suggested feature-branch pattern, configured worktree base path, workflow preference, allowed workflow modes, default remote, draft-PR setting, and policy source for an authorized repositorygit_statusfor an authorized repositorygit_addfor repository-relative paths inside an authorized repository; it rejects absolute paths and repository-escaping paths like../xgit_stageas a discoverability alias forgit_addwhen the caller is looking for "stage" rather than "add"git_commitwith a normal commit message on an allowed branch; it rejects empty commit messages and empty commitsgit_fetchto fetch a plain branch name from the detected remote; it updates local fetch metadata and remote-tracking state only, does not allow arbitrary fetch arguments or refspecs, and uses an explicit branch when provided or the detected base branch otherwisegit_sync_baseto merge the detected remote base branch into the current allowed branch; it requires a clean worktree, does not allow arbitrary refs or merge flags, and aborts on conflict before returning an errorgit_pull_current_branchto fetch and merge the current branch from the detected remote into the current allowed branch; it requires a clean worktree, does not allow arbitrary refs or merge flags, and aborts on conflict before returning an errorgit_worktree_addto create a linked worktree for a new allowed branch at an explicit absolute path; it fetches the explicit or detected base branch first, does not allow arbitrary refs, and enforcesgit_worktree_base_pathwhen configuredgit_branch_create_and_switchto create a local branch from an explicit or detected upstream base and switch to it; it rejects requested branch names that do not match the configured allowed branch patternsgit_branch_switchto switch to an existing local branch when the worktree is clean; it does not create branches or allow detached checkoutsgit_pushto push the current branch to the detected remote; it only pushesHEADtorefs/heads/<current-branch>and does not allow arbitrary refspecs, force-like behavior, delete pushes, or unrelated branch pushesgh_pr_create_draftto create a draft PR for the current branch using an explicit base or the detected default branch; it is draft-only and requires a non-empty title
Mutating tools reject detached HEAD.
When the global config file is missing, runtime tools remain registered. They can still authorize repositories through .git-unleash.yaml, and once the YAML exists, the next tool call reloads it from disk.
Some operations resolve defaults at runtime instead of requiring everything to be pinned in config.
git_fetch,git_sync_base,git_pull_current_branch,git_push,git_worktree_add,git_branch_create_and_switch, andgh_pr_create_draftresolve the remote by preferring configureddefault_remote, then the current branch remote, thenorigingit_fetch,git_worktree_add,git_branch_create_and_switch, andgh_pr_create_draftaccept an explicit branch or base inputgit_fetch,git_sync_base,git_worktree_add,git_branch_create_and_switch, andgh_pr_create_draftresolve their default branch or base by preferring the remote HEAD branch and falling back to the GitHub repository default branch when no explicit input is provided
This keeps the tools constrained while still working across repositories that use different default branches or remotes.
If you want to branch from main but only allow mutations on personal feature branches:
defaults:
allowed_branch_patterns:
- "^main$"
feature_branch_pattern: "<user>/<feature-name>"
workflow_mode: worktree
allowed_workflow_modes:
- feature_branch
- worktree
repositories:
- path: ~/projects/codex-git-unleash-mcp
- path: ~/projects/codex-git-unleash-mcp-enterprise
allowed_branch_patterns:
- "^feature/[a-z0-9._-]+$"
workflow_mode: feature_branch
allowed_workflow_modes:
- feature_branchIn that example, workflow_mode tells agents to start with the worktree setup flow by default, while allowed_workflow_modes keeps both worktree and feature_branch available when the caller needs either one.
If you want some repositories to stay on their base branch instead of creating a worktree or feature branch:
repositories:
- path: ~/projects/dm-cv-on-steroids
workflow_mode: current_branch
allowed_workflow_modes:
- current_branch
allowed_branch_patterns:
- "^main$"
- path: ~/dot.files
workflow_mode: current_branch
allowed_workflow_modes:
- current_branch
allowed_branch_patterns:
- "^main$"If you want to bootstrap the config and then add this repository incrementally, a minimal progression is:
Use this progression only when the caller is trusted to change MCP policy. Exposing either configuration mutation tool delegates that authority to its caller.
- Call
config_bootstrapwith defaults such asfeature_branch_pattern, the preferredworkflow_mode, orallowed_workflow_modes. - Call
config_upsert_repowithrepo_path, and optionallygit_worktree_base_path,default_remote,allowed_branch_patterns, the preferredworkflow_mode, orallowed_workflow_modes. - Use the Git tools against that repository; they will reload the YAML on each call.