Skip to content

fix(git): show non-ASCII paths unescaped in git_status and git_diff* - #4879

Open
tsurutanmen wants to merge 2 commits into
modelcontextprotocol:mainfrom
tsurutanmen:fix/git-quotepath-non-ascii
Open

tsurutanmen wants to merge 2 commits into
modelcontextprotocol:mainfrom
tsurutanmen:fix/git-quotepath-non-ascii

Conversation

@tsurutanmen

Copy link
Copy Markdown

Description

git_status, git_diff_unstaged, git_diff_staged and git_diff return non-ASCII file names as quoted octal escapes, because git's default core.quotepath=true applies to their output. A file named 日本語.txt shows up as:

Untracked files:
	"\346\227\245\346\234\254\350\252\236.txt"

and in diffs as diff --git "a/\346\227\245\346\234\254\350\252\236.txt" .... An LLM client cannot map that back to the real file, so any follow-up call such as git_add with the escaped name fails. This PR runs these four commands with -c core.quotepath=false, so the same file shows up as 日本語.txt.

git_show is unchanged. It builds its output from GitPython's Diff objects, which already decode the paths.

Server Details

  • Server: git
  • Changes to: tools (git_status, git_diff_unstaged, git_diff_staged, git_diff)

Motivation and Context

This affects any repository with non-ASCII file names: Japanese, Chinese, Korean, accented Latin, emoji, and so on. The setting only changes how git prints paths. It does not touch repository config, and it does not change output for ASCII paths.

How Has This Been Tested?

  • Added 4 tests, one per tool, with a non-ASCII file name. They set core.quotepath=true in the test repository, so the result does not depend on the user's global config. All 4 fail without the fix and pass with it.
  • Full suite: 51 passed. pyright and ruff check are clean.
  • With an LLM client: I ran the server under claude -p (Claude Code) against a repository containing an untracked 日本語.txt and had the model call git_status. Before the fix, the output contained "\346\227\245\346\234\254\350\252\236.txt". After the fix, it contained 日本語.txt.

Tested on Windows 11 with git for Windows and Python 3.11. On Windows, the existing test_repository fixture teardown raises PermissionError from shutil.rmtree while GitPython still holds handles. This happens before and after this change and is unrelated to it. CI on Linux is not affected.

Breaking Changes

None. ASCII output is identical. Clients that were parsing the escaped form will now receive the real UTF-8 path.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follows MCP security best practices
  • I have updated the server's README accordingly
  • I have tested this with an LLM client
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options

Additional context

-c core.quotepath=false is passed per command through GitPython (repo.git(c=...)), so it applies only to that single invocation.

🤖 Generated with Claude Code

git's default core.quotepath=true makes `git status` and `git diff`
print non-ASCII paths as quoted octal escapes, e.g.
"\346\227\245\346\234\254\350\252\236.txt" for 日本語.txt. Clients
cannot map these back to the real file, so pass
`-c core.quotepath=false` for git_status, git_diff_unstaged,
git_diff_staged and git_diff. git_show already reports decoded paths
through GitPython's Diff objects and is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chrikrah chrikrah left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tsurutanmen the four new cases fail on the base commit and pass on e067aed.

non-blocking: ESCAPED_NON_ASCII_FILENAME at src/git/tests/test_server.py:190 uses Python octal escapes. The constant holds thirteen characters of mojibake that git never emits, so the four not in assertions also pass on f46d957. Git writes a forty-character token. r"\346\227\245\346\234\254\350\252\236.txt" makes those assertions bite.

#4848 replaces the same three return repo.git.diff(...) lines and adds its tests at the same insertion point. git merge-tree --write-tree across the two heads exits 1 with a conflict in both files.

Verification

Exported e067aed with git archive, then a fresh uv sync --frozen --dev. Inside pytest I asserted mcp_server_git.__file__ sits in that export.

$ cd src/git && uv run pytest tests -q
51 passed in 6.43s

# src/mcp_server_git/server.py restored to f46d9578, the four new tests kept
$ uv run pytest tests -q
4 failed, 47 passed in 13.05s
FAILED tests/test_server.py::test_git_status_non_ascii_filename
FAILED tests/test_server.py::test_git_diff_unstaged_non_ascii_filename
FAILED tests/test_server.py::test_git_diff_staged_non_ascii_filename
FAILED tests/test_server.py::test_git_diff_non_ascii_filename

# the same reverted tree, both candidate constants against git_status output
$ uv run python -c 'print(RAW in out, OCT in out)'
True False

$ git merge-tree --write-tree refs/remotes/pr/4879 refs/remotes/pr/4848; echo $?
CONFLICT (content): Merge conflict in src/git/src/mcp_server_git/server.py
CONFLICT (content): Merge conflict in src/git/tests/test_server.py
1
# not run: everything outside this file, and any run on Windows or macOS

src/git carries no CODEOWNERS entry, and @olaservo merged six of the changes touching these two files. @olaservo this one is ready to merge as it stands.

@tsurutanmen do you want the raw-string constant folded in here, or in a pull request of its own?

The constant used Python octal escapes, so it held 13 characters git
never emits and the four not-in assertions passed even before the fix.
As a raw string it is the 40-character token git writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 5, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 88150d6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@tsurutanmen

Copy link
Copy Markdown
Author

Thanks for checking this so carefully. Folded in here as 88150d6: the constant is now a raw string, so it is the 40-character token git prints. Against the pre-fix git status output it is found, where the old one was not, so the four not in assertions now bite.

On #4848: happy to rebase onto whichever lands first.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants