Plugin marketplace `git checkout` uses process CWD as work-tree — leaks plugin tree into the caller's directory

Status Fixed / completed
Reported on v2.1.161
Maintainer reply None cached
Activity 3 comments · opened Jun 3, 2026 · closed Jun 3, 2026

Summary

When claude-code installs/resolves a plugin whose marketplace source is an external git repo, it runs git checkout <sha> without pinning --work-tree (or -C), so the checkout's work tree is the git process's current working directory. In code paths where that cwd is not the temporary clone directory, the entire plugin file tree is materialized into the caller's working directory via O_WRONLY|O_CREAT|O_EXCL.

For an interactive user this is usually invisible (the install path happens to chdir into the temp clone first). But when claude-code is launched non-interactively from another working directory (e.g. an agent/automation harness that spawns claude inside a project repo), a plugin-resolution that takes the un-pinned path dumps files like package.json, .mcp.json, README.md, .claude-plugin/plugin.json, skills/**, etc. into that project directory. Because the writes use O_CREAT|O_EXCL, only missing filenames are created (existing files survive), which is why it shows up as a scatter of foreign/empty-looking config stubs and can break the next tool that reads e.g. package.json in that dir.

  • Version: 2.1.161 (native install, Linux)
  • Mechanism reproduced live under strace -f (see below)

Root cause / evidence

Plugin resolution does a checkout-less shallow clone of the external source into a temp dir, then a separate git checkout <sha>:

execve("/usr/bin/git", ["git", "-c","core.sshCommand=...", "clone", "--depth","1",
   "--recurse-submodules","--shallow-submodules","--no-checkout","--",
   "https://github.com/AikidoSec/aikido-claude-plugin.git",
   "<CLAUDE_CONFIG_DIR>/plugins/cache/temp_git_<ts>_<rand>"]) = 0

execve("/usr/bin/git", ["git", "checkout", "<sha>"]) = 0      # <-- no --work-tree, no -C

The git checkout then writes relative to the process cwd (AT_FDCWD):

openat(AT_FDCWD, ".claude-plugin/plugin.json",   O_WRONLY|O_CREAT|O_EXCL, 0666) = 4
openat(AT_FDCWD, ".claude-plugin/marketplace.json", O_WRONLY|O_CREAT|O_EXCL, 0666) = 4
openat(AT_FDCWD, ".mcp.json",                    O_WRONLY|O_CREAT|O_EXCL, 0666) = 4
openat(AT_FDCWD, "README.md",                    O_WRONLY|O_CREAT|O_EXCL, 0666) = 4
openat(AT_FDCWD, "skills/scan/SKILL.md",         O_WRONLY|O_CREAT|O_EXCL, 0666) = 4
...

Because the work tree is determined by cwd, the destination is correct only as long as the caller chdir'd into the temp clone first. The behavior is fragile by construction — any resolution path (or future refactor, or a concurrent operation that changes cwd) that runs the checkout without that chdir will materialize the plugin tree wherever the claude process happens to be running. The leftover temp_git_* clone dirs contain a bare .git with no work tree, confirming the checkout's output went elsewhere.

Reproduction (mechanism)

CFG="$(mktemp -d)"; WORK="$(mktemp -d)"; ( cd "$WORK" && git init -q )
# add the official marketplace into an isolated config, then install a plugin
# whose marketplace source is an external git repo:
CLAUDE_CONFIG_DIR="$CFG" claude plugin marketplace add anthropics/claude-plugins-official
cd "$WORK"
CLAUDE_CONFIG_DIR="$CFG" strace -f -e trace=openat,chdir,execve -o /tmp/s.log \
  claude plugin install aikido@claude-plugins-official
grep -E 'execve\("/usr/bin/git".*(clone|checkout)|openat\(AT_FDCWD, "[^/]' /tmp/s.log

This shows the clone --no-checkout + git checkout <sha> pair and that the checkout writes via openat(AT_FDCWD, "<relative path>", O_WRONLY|O_CREAT|O_EXCL) — i.e. the work tree is the process cwd. (In the install path claude chdirs into the temp clone, so files land correctly; the bug surfaces on the resolution paths that don't, which is what gets observed as files appearing in a caller's project directory.)

Suggested fix

Pin the work tree explicitly on every plugin/marketplace git checkout/read-tree rather than relying on the inherited cwd — e.g. git -C <tmpCloneDir> checkout <sha> or git --git-dir=<tmp>/.git --work-tree=<tmp> checkout <sha> (and/or spawn with cwd: <tmpCloneDir>). That makes the destination independent of wherever the claude process was launched.

Workaround (for harness authors)

Launch the spawned claude with an isolated, plugin-free CLAUDE_CONFIG_DIR (a fresh dir carrying only a symlink to .credentials.json, no settings.json/plugins/). With no enabled plugins / known marketplaces, no external source is ever resolved, so nothing can be checked out into the worker's cwd. (DISABLE_AUTOUPDATER=1 does not prevent this — it gates the binary self-updater, not the plugin-marketplace machinery.)

View original on GitHub ↗

3 Comments

github-actions[bot] · 4 months ago

Found 1 possible duplicate issue:

  1. https://github.com/anthropics/claude-code/issues/63680

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

papercupai · 4 months ago

Closing as could-not-reproduce on the current release.

Following up as the reporter after a deeper investigation on claude-code 2.1.161 (Linux). I was unable to reproduce an actual leak into the caller's working directory across every path I tried:

  • single external-url-source plugin install (claude plugin install <plugin>@<marketplace>)
  • concurrent resolution of several enabled external plugins at startup
  • forced re-resolution (an enabled plugin with its cache removed)
  • read-only plugin cache
  • running claude -p under a restrictive --settings sandbox (writes confined to cwd)

In every case claude chdirs into the temporary clone directory before git checkout <sha>, so the checkout's files land in the clone (and then the install path), not the process cwd. Importantly, the openat(AT_FDCWD, "<relative>", O_WRONLY|O_CREAT|O_EXCL) calls I originally cited as a "leak signature" are just normal checkout writes — in a concurrent run I observed 200+ of them and zero landed in the caller's directory, because the cwd was correctly pinned.

The underlying observation still stands as a latent fragility: git checkout <sha> is invoked without an explicit --work-tree/-C, so it relies on the inherited cwd; pinning it explicitly would be cheap defensive hardening. But since I can't demonstrate an actual leak on the current version, I'm closing rather than leave a low-confidence report open. Happy to reopen with a concrete reproduction if it resurfaces. Thanks!

github-actions[bot] · 1 month ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.