●OPUS48 — The default model on Bedrock, Vertex, and AWS is now Claude Opus 4.8. Opus 4.8 and Haiku 4.5 are also in the Messages API, with prompt caching and extended thinking●STREAM — A Claude Code stability and quality update landed: subagent text over stream-json, stronger permission and hook handling, and better background-agent reporting●FASTEND — Fast mode for Claude Opus 4.7 is removed on July 24. After that, speed: 'fast' returns an error, so migrate to fast mode on Opus 4.8●TEACH — Claude for Teachers is here, giving verified US K-12 educators free premium access, plus curriculum connections aligned to standards in all 50 states and new education connectors●FIX — The update fixes issues across Chrome, Windows, Bedrock, Vertex, hooks, and session recovery, and speeds up terminal rendering●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●OPUS48 — The default model on Bedrock, Vertex, and AWS is now Claude Opus 4.8. Opus 4.8 and Haiku 4.5 are also in the Messages API, with prompt caching and extended thinking●STREAM — A Claude Code stability and quality update landed: subagent text over stream-json, stronger permission and hook handling, and better background-agent reporting●FASTEND — Fast mode for Claude Opus 4.7 is removed on July 24. After that, speed: 'fast' returns an error, so migrate to fast mode on Opus 4.8●TEACH — Claude for Teachers is here, giving verified US K-12 educators free premium access, plus curriculum connections aligned to standards in all 50 states and new education connectors●FIX — The update fixes issues across Chrome, Windows, Bedrock, Vertex, hooks, and session recovery, and speeds up terminal rendering●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
A Committed Symlink That Points Outside the Worktree — Auditing Repos Before You Let AI Spin Up Parallel Trees
Claude Code 2.1.212 fixed a bug where a committed symlink under .claude/worktrees could be followed during worktree creation and write outside the repo. The patch closes the following side. Here is an audit script for the committed side, plus a quarantine workflow.
One line in the Claude Code 2.1.212 changelog stopped me. It fixed a bug where a symlink committed under .claude/worktrees could be followed during worktree creation and end up writing a file outside the repository. It is already fixed.
But before feeling relieved, I thought about what I actually hand to Claude Code. In my own automation as an indie developer, I clone several repositories every day, cut worktrees, and run work in parallel on top of them. I had never once looked at the contents of those repositories through the lens of symlinks.
The patch closes the following side — it stops the tool from traversing a malicious link. But whether a dangerous link is committed in the first place is something only the person handling the repo can inspect. A patch does not guard every AI tool and every operation, and worktree creation is not the only path that can follow a link. This is a record of how I run that inspection on my own machine.
Git commits a symlink as "the target string"
One premise first. Git does not treat symlinks specially — it stores them as a file whose content is the link's target string. Only the file mode differs from a normal file (100644): it becomes 120000.
Here is what that looks like in a real repo.
# Deliberately create a dangerous example (for testing; we delete it right after)ln -s /etc/hosts ./link-to-hostsgit add ./link-to-hosts# See how git recorded itgit ls-files -s ./link-to-hosts# It enters as 120000, not 100644:# 120000 a1b2c3... 0 link-to-hosts# The blob content is literally the target stringgit cat-file blob :./link-to-hosts# /etc/hosts
A 120000 entry has, as its blob content, the entire link target. Whether it is /etc/hosts or ../../../../home/user/.ssh/authorized_keys, as long as it is a string, git commits it without complaint. There is no validation here. So a link pointing outside the repository can slip in looking exactly like any ordinary change.
core.symlinks makes this trickier. If it is true (the default in most environments), checkout restores the link as a real symlink. If false, the target string is written out as a normal file and nothing follows it. The same commit becomes either "a link that gets followed" or "plain text" depending on config. That gap is what makes inspection hard.
Why worktree creation could escape
Writing across a symlink to reach outside is not specific to worktrees. In general, if a write path passes through a symlink, the OS writes to wherever the link resolves. If b in a/b/c is a link to /tmp/evil, a write to a/b/c lands in /tmp/evil/c.
During worktree creation, the tool writes bookkeeping files under .claude/worktrees. If part of that path is a committed link pointing outside the repo, the write leaks out of the repository. Depending on checkout order, the link may already exist as a concrete entry at the moment of the write — the classic TOCTOU (time-of-check to time-of-use) gap.
The 2.1.212 fix stops that traversal on the tool side. It deserves credit. But the question I had to own was a different one: "Am I letting AI touch a repository that contains such a link in the first place?" The clone source is not always under my control. If I open forks, templates, or vendored external repos as worktrees, the surface to inspect grows.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦An audit script that surfaces only the dangerous symlinks in a repo — absolute paths, links escaping the root, links into managed directories (copy-paste ready)
✦How git commits a symlink as its target string, and the following mechanism that let worktree creation escape the repo
✦Even on a patched version: the clone → audit → quarantine → worktree order, and the minimal check to drop into CI
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
An audit script that surfaces only dangerous links
You do not need to delete every symlink. Legitimate links (relative links inside the repo) are used all the time. What you want to surface is only "points outside the repo," "absolute paths," and "points into .git or .claude."
Take the 120000 entries from git ls-files -s, normalize each blob's content (the target), and flag any that resolve outside the repo root. Because it reads the index rather than the working tree, it runs even on a fresh clone before checkout.
#!/usr/bin/env bash# audit-symlinks.sh — list only the dangerous committed symlinks# Usage: run at the repo root: bash audit-symlinks.shset -euo pipefailROOT="$(git rev-parse --show-toplevel)"DANGER=0# Pull out only the 120000 (symlink) entriesgit ls-files -s | awk '$1 == "120000" { $1=$2=$3=""; sub(/^ +/,""); print }' \| while IFS= read -r path; do # blob content = the link target string target="$(git cat-file blob ":${path}")" verdict="" case "$target" in /*) verdict="absolute path" ;; # starts with / esac # Normalize the target relative to the link's location; flag if outside root linkdir="$(dirname "${ROOT}/${path}")" resolved="$(cd "$linkdir" 2>/dev/null && \ python3 -c "import os,sys;print(os.path.normpath(os.path.join(os.getcwd(), sys.argv[1])))" "$target" 2>/dev/null || true)" case "$resolved" in "$ROOT"|"$ROOT"/*) : ;; # under root is fine *) [ -n "$verdict" ] || verdict="escapes the root" ;; esac # Anything into .git / .claude is dangerous, no exceptions case "$resolved" in "$ROOT"/.git|"$ROOT"/.git/*|"$ROOT"/.claude|"$ROOT"/.claude/*) verdict="into a managed dir" ;; esac if [ -n "$verdict" ]; then printf '⚠️ %-40s → %s [%s]\n' "$path" "$target" "$verdict" DANGER=1 fi doneexit "$DANGER"
The script exits with code 1 if it finds even one dangerous link, so you can wrap it with if ! bash audit-symlinks.sh; then … in CI or a hook. Running it across my four repos, dangerous links came back at zero. Confirming zero was itself the payoff: the state moved from "never looked" to "looked and found nothing wrong."
What counts as "dangerous" — the criteria
Here are the criteria as a table. This is where you tune so you do not sweep up legitimate links.
Link target
Example
Verdict
Absolute path
/etc/hosts
Dangerous (behavior varies by environment)
Relative, escapes root
../../../.ssh/id_ed25519
Dangerous (leaves the repo)
Into a managed dir
./x → .git/hooks
Dangerous (can be repurposed to inject hooks)
Relative, under root
./docs/logo.svg
Allowed (usually legitimate)
Target does not exist
dangling link
Review (confirm intent first)
One honest caveat. This audit inspects committed links. It does not cover links created at runtime, or the TOCTOU window where core.symlinks resolution timing matters. The audit narrows the entrance; it is not a substitute for the fix that stops traversal itself. That is why, to me, applying the patch and running the audit are two things you do together, not one instead of the other.
Quarantine first, then create the worktree
When you open a low-trust repository, you can seal off traversal at clone time. Cloning with core.symlinks=false expands links as ordinary files whose content is the target string, so the OS does not follow them.
# For an untrusted clone, first land links as "plain text"git -c core.symlinks=false clone "$REPO_URL" ./untrustedcd ./untrustedbash /path/to/audit-symlinks.sh || { echo "Dangerous link detected. Aborting worktree creation." exit 1}# Only after the audit passes, re-enable links if needed and cut a worktree
core.symlinks=false affects legitimate links too. A project that builds on symlinks will not work while that setting stands. So use it in the order "seal untrusted clones, inspect, then re-enable if clean." In my automation, I apply this clone only to externally sourced repos and leave my own repos on a normal clone. Locking everything down breaks legitimate uses, so splitting the target by provenance turned out to be the practical line.
The practice of having Claude Code spin up many worktrees is covered in Parallel Development with Claude Code Worktrees. Think of this article as the inspection layer that sits in front of it.
Wiring the check into clone and CI
The check works when it is pinned to the path, not when you remember to run it by hand. Here is where to drop it in.
Where
What
Effect
Clone wrapper
Clone external sources with core.symlinks=false
Seals traversal at the entrance
Just before worktree add
Run audit-symlinks.sh; abort on failure
Never opens a branch with dangerous links
CI (pull request)
Run the same script as one job
Rejects newly introduced dangerous links
Scheduled
Run weekly across every repo you touch
Catches links you already pulled in
The minimal shape for wrapping worktree creation looks like this. If the audit does not pass, the branch never opens.
The added script is a few dozen lines; the CI job is one. That turned a "path I never looked at" into a "path I look at, and open only if it passes." When the weight of the implementation and the weight of the incident it prevents are this lopsided, finding that asymmetry is a small pleasure.
Closing
If you try just one thing, run audit-symlinks.sh once at the root of a repo you currently let AI touch. In most cases the result is zero. That zero confirms there is no danger, and it is also a step past "never looked." If something does come up, the fact that the one link was found before cutting a worktree is itself the argument for pinning the check to the path.
I would not call myself a security expert, and this audit is not complete either. Still, rather than relying on the patch alone, I add one layer on my own side. I wrote this hoping to share that small habit. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.