Skip to content

Symlink corruption: get_file_contents follows symlinks but create_or_update_file does not #2997

Description

@kujiy

Describe the bug

While using Claude Fable 5, I asked the agent to edit a text file that was a symlink to another file.

get_file_contents returned the target file's content and a SHA without indicating that the requested path was a symlink. Claude then passed the edited content and returned SHA to create_or_update_file for the same path.

The write succeeded, but updated the symlink blob rather than the target file, while preserving Git mode 120000. In the original incident, approximately 15 KB of Markdown became the symlink target and the resulting commit could not be checked out.

This does not happen in a normal filesystem-based editing workflow, where reads and writes both follow the symlink. It is specific to workflows that use GitHub MCP or the GitHub API for the write, including cases where an agent reads from a local checkout and uses MCP only to commit the update.

Affected version

The command that works with the current image is:

$ docker run -i --rm ghcr.io/github/github-mcp-server:latest --version
GitHub MCP Server
Version: v1.8.0
Commit: ca8ab52dcc45b86fae190398178fd22edb7b1362
Build Date: 2026-07-30T13:28:24Z

Steps to reproduce the behavior

  1. Create and push a regular file and a symlink to it:

    head -c 4097 /dev/zero | tr '\0' A > file1.txt
    ln -s file1.txt file2.txt
    git add file1.txt file2.txt
    git commit -m "Add symlink"
    git push
  2. Call get_file_contents for file2.txt. It returns the 4,097-byte contents of file1.txt, a file SHA, and no indication that the requested path is a symlink.

  3. Call create_or_update_file for file2.txt with the returned SHA and content.

  4. Inspect the result:

    git ls-tree HEAD file2.txt
    git cat-file -p HEAD:file2.txt

    The mode remains 120000, but the blob contains the submitted file content instead of file1.txt.

  5. Clone or check out the resulting commit. Checkout fails with File name too long.

Expected vs actual behavior

Expected: before writing, the agent is told that GitHub API writes operate on the exact Git path and do not follow symlinks like filesystem writes. This should still allow intentional symlink updates: to edit the linked file, write to its target path; to change the symlink, write the new target path.

Actual: the read follows the symlink, while the write updates the symlink blob. The different semantics are not exposed to the agent, and SHA validation accepts the value returned by the read.

A possible fix is to document the exact-path behavior in both the server-level MCP instructions and the tools/list descriptions of mutating file tools. This informs the agent before any file tool is called, including when it reads the content locally and uses MCP only for the write.

I would be happy to submit a pull request once the maintainers confirm the preferred wording and scope.

Logs

The following output was captured using the official v1.8.0 Docker image. Repository identifiers, SHAs, URLs, and most of the 4,097-byte content have been redacted.

get_file_contents followed file2.txt and returned the 4,097-byte contents of file1.txt as a text resource:

{"jsonrpc":"2.0","id":2,"result":{"content":[
  {"type":"text","text":"successfully downloaded text file (SHA: <redacted>)"},
  {"type":"resource","resource":{"uri":"repo://<redacted>/contents/file2.txt","mimeType":"text/plain; charset=utf-8","text":"AAAA... <4,093 bytes redacted>"}}
]}}

create_or_update_file accepted the same content and returned success with size: 4097:

{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"{\"content\":{\"name\":\"file2.txt\",\"path\":\"file2.txt\",\"sha\":\"<redacted>\",\"size\":4097,\"html_url\":\"<redacted>\"},\"commit\":{\"sha\":\"<redacted>\",\"message\":\"Reproduce symlink write behavior\",\"html_url\":\"<redacted>\"}}"}]}}

After the write, the Contents API reported the path as a 4,097-byte symlink, and the Git tree still had mode 120000:

{"type":"symlink","size":4097,"sha":"<redacted>","target":"AAAA... <4,093 bytes redacted>"}
{"path":"file2.txt","mode":"120000","type":"blob","sha":"<redacted>","size":4097}

This cannot happen on an ordinary file system. The MCP call itself produced no error. Cloning the resulting branch fetched the commit, then failed during checkout:

Cloning into '<redacted>'...
error: unable to create symlink file2.txt: File name too long
fatal: unable to checkout working tree
warning: Clone succeeded, but checkout failed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions