Claude Code Just Patched Its Third Symlink Deny-Rule Bypass in Eleven Months
A deny rule looks like a wall. Three fixes in eleven months, plus a separate symlink sandbox escape rated High, say it is closer to a set of independent doors locked by different people on different days.
A deny rule in Claude Code reads like a promise. You put Read(./.env) in your settings file, and the agent cannot read your environment file. That is the whole mental model, and it is why people feel comfortable pointing an autonomous coding agent at a repository that also happens to contain their credentials.
That promise has now been walked around three separate times in eleven months.
September 2025, disclosed October 3: CVE-2025-59829. Claude Code failed to account for symlinks when checking deny rules. If you denied a file and the agent could reach a symlink pointing at it, the agent read the file. Fixed in 1.0.120, published to npm September 19, 2025, reported by a researcher going by vinai on HackerOne.
January 2026, disclosed February 6: CVE-2026-25724. Claude Code failed to strictly enforce deny rules from settings.json when files were reached through symbolic links. The advisory's own example is /etc/passwd. Fixed in 2.1.7, published January 13, 2026, reported by ofirh on HackerOne.
August 2026, disclosed nowhere: Claude Code 2.1.251, published to npm on August 28. One line among seventy-one: "Fixed Grep and Glob not applying Read(...) deny rules to files reached through a symlinked search path." No CVE. No advisory. No severity.
Both CVEs carry CWE-61, symbolic link following, and the 2026 one adds CWE-285, improper authorization. The third has no CVE, so it carries nothing. And neither advisory says which part of the harness it lived in. Only the August changelog line names its tools, Grep and Glob. That the first two were found separately, four months apart, by different researchers, is the tell.
Deny rules are not even the whole symlink story in this product. CVE-2026-39861, fixed in 2.1.64 on March 3 and disclosed April 21, is a sandbox escape through symlink following that allowed arbitrary file writes outside the workspace. GitHub rates it High at 7.7. GitLab rates it 10.0, Critical. Different failure, same primitive.
The wall was never a wall
Here is what I think is actually going on, and it is more interesting than "vendor ships bug."
In a chat product, a permission check is a single gate. In an agent harness, it is a cross-cutting concern spread across every tool that can touch a path. Read has one. Write has one. Edit has one. Grep has one. Glob has one. The Workflow tool has one. A plugin marketplace entry has one. Every new tool that learns to open a file inherits the responsibility to canonicalize the path first, and inherits it by convention rather than by construction.
So the deny list is not a policy. It is a set of independent implementations of a policy, and the count resets every time the product grows a new way to read a byte off disk.
Look at the rest of 2.1.251 with that lens and the release stops looking like a grab bag. Four other entries are the same shape:
File tools followed a symlink that was swapped inside the working directory after the permission check passed, which could read or write outside the approved location. That one is a genuine race, not a missing canonicalization. The check was correct at the moment it ran and wrong a moment later.
The Workflow tool read a scriptPath outside what the session may read before the permission check ran, and then quoted that content back in its error message. Denied data escaped through the failure path rather than the success path.
Plugin commands declared in a marketplace entry could point outside the plugin directory. Those now get rejected with a path-traversal error.
Bash permission checks auto-approved commands that assign an arithmetic expression to an integer shell variable, with OPTIND=1/0 and RANDOM=2+2 given as examples. The classifier looked at a command, decided it was harmless, and was reading a shape it did not fully understand.
And one that has nothing to do with the filesystem but everything to do with the same assumption: managed-settings disableAutoMode arriving mid-session did not move an already-running auto-mode session back to default mode. The policy changed. The session did not hear about it.
That is the actual lesson sitting in this release. A permission decision has a lifetime, and almost everywhere in this harness the lifetime was assumed to be forever.
What the numbers say, and what they do not
Severity here depends entirely on who you ask, and the disagreement is larger than the numbers usually are.
GitHub's advisory database rates both deny-rule CVEs Low, CVSS v4 base 2.3 apiece. GitLab's database disagrees with both: 6.5 Medium for CVE-2025-59829 and 7.5 High for CVE-2026-25724, under CVSS 3.1. Same bug, same description, four and a half points apart. Severity scores are opinions with arithmetic attached, and you should stop treating a single badge as a fact.
EPSS puts CVE-2026-25724 at 0.397% as of August 30, around the 32nd percentile for likelihood of exploitation in the next thirty days. Low, and worth pinning to a date, because EPSS re-scores daily.
The August fix has no CVE at all. It is a changelog line, so it has no severity to argue about and no statement about whether anyone found it in the wild.
Every one of these needs an attacker who can already place or swap a symlink inside your working directory. That is a real precondition and it should lower your heart rate.
It should not lower it very far. Your agent writes into that directory constantly. So does npm install. So does every postinstall script, every generated file, every dependency you have never read, and every repository you cloned because a coworker linked it in Slack. The precondition "someone can create a file in your project folder" describes normal Tuesday afternoon.
The timing is worth noticing too. Anthropic shipped the 2.1.7 fix on January 13 and the advisory went public on February 6, twenty-four days later. The 2.1.64 sandbox-escape fix shipped March 3 and was disclosed April 21, seven weeks later. Users on auto-update got both patches before anyone told them there was a hole. That ordering is correct, and it is also why the changelog is the only place the August one exists so far.
Put this into practice
None of the following requires you to stop using coding agents. It requires you to stop treating the deny list as load-bearing.
Move real secrets out of reach instead of onto a list. A deny rule is best-effort inside a filesystem that keeps moving. A file the process cannot open is not. Put credentials outside the working tree, or run the agent as a user that has no read permission on them, or run it in a container that never mounts them. The operating system has been solving this for forty years and it does not have a symlink bug from last Tuesday.
Write your policy assuming search tools are read tools. Grep and Glob return file contents and file paths. If your threat model says "the agent must not read X," your deny rules have to cover the tools that read X sideways. In this product they did not, at least up to the release that fixed it on August 28. Nothing public says when that gap opened, which is its own kind of answer.
Take the auto-update. This is the unglamorous one. Two of these three fixes reached most people before the advisory did, purely because the default is to update. If you pin the version for reproducibility, you have taken ownership of the patch lag, and you should be reading the changelog every release rather than quarterly.
Treat a model switch as a policy event. The same release added PreModelSwitch and PostModelSwitch hooks that can block, confirm, or annotate a switch. If your session's safety assumptions were tuned against a particular model, a mid-session change moves the ground underneath them. Start with annotate, not block. A hook that blocks too eagerly turns a cheap fallback into a stalled session, and it runs on your machine on the hot path of every switch.
Read your error messages as an exfiltration channel. The Workflow bug leaked out-of-scope content by quoting it in an error string. Anywhere your harness echoes a path or a file body into a failure message, that message is an output, and it deserves the same filter the success path gets.
If you build your own harness, canonicalize once and centrally. Resolve the real path at the moment of the syscall, not at the moment of the grant, and put that resolution in one function every tool must call. The bug that keeps coming back here is not a symlink bug. It is a distributed-responsibility bug wearing a symlink costume.
Honest limitations
I have not reproduced any of this. Every detail about 2.1.251 comes from Anthropic's own release notes, read from the raw changelog rather than a GitHub page, with the publish timestamp confirmed against the npm registry. If the changelog description of a fix is imprecise, so is mine.
I am also reading six entries in a seventy-one-item release as a single pattern, and that is an interpretation, not a finding. Anthropic never called it a pattern. Large releases contain coincidences, and a person looking for time-of-check problems will find time-of-check problems.
The count of three is a count of deny-rule instances specifically, and that narrowing is a choice I made. Widen the frame to CWE-61 in this package and the number goes up. Beyond CVE-2026-39861, the advisory database also lists CVE-2026-46406, an insecure temp file in /copy described as a symlink-based file write and rated 6.1 Medium, and CVE-2026-55607, a worktree path confusion issue explicitly involving symlink manipulation and rated 8.8 High. I excluded them because they are boundary escapes rather than deny-rule bypasses. A reasonable person would call that hair-splitting and count five.
Severity is genuinely contested rather than settled, so anyone quoting a single number, including me, is picking a side.
And this is a count of the ones with public paper. There may have been more, fixed without a note, in a product that ships several releases a week.
What to do with this
The useful takeaway is not "Claude Code is unsafe." It is that agent harnesses are now large enough that their permission systems have surface area, and surface area gets audited by whoever bothers.
So bother. Open your settings file today and ask a different question than the one you asked when you wrote it. Not "did I deny the right things," but "what happens to each of these files if the deny rule stops working for one tool, for an unknown number of months, and nobody notices until a stranger on HackerOne does."
If the answer is "nothing much," you have designed well. If the answer makes your stomach drop, you have a weekend project, and it is a smaller one than you think.
Sources: Claude Code CHANGELOG (raw, cache-busted), GHSA-66m2-gx93-v996 / CVE-2025-59829, GHSA-4q92-rfm6-2cqx / CVE-2026-25724, GHSA-vp62-r36r-9xqp / CVE-2026-39861, the GitLab Advisory Database entries for the same CVEs, and the npm registry publish timestamps for @anthropic-ai/claude-code 1.0.120, 2.1.7, 2.1.64, and 2.1.251. Severity and EPSS figures read August 30, 2026.