← Tools I Use

SymJack: the symlink RCE that broke six AI coding agents at once

2026-06-05 · ~7 min read · Conor Dobbs

A researcher took one attack chain and pointed it at six different AI coding agents from six different companies. All six fell. The agents were Claude Code, Cursor's agent CLI, GitHub Copilot CLI, OpenAI Codex CLI, Gemini/Antigravity CLI, and Grok's build CLI. The technique got named SymJack, and it is worth understanding even if you only ever use one of those tools, because the root cause is a design assumption shared across the whole category.

The short version: you approve what looks like copying a few media files. The files are symlinks. The write lands on your agent's config directory instead of where the prompt said it would. On the next restart, the agent loads a planted MCP server and runs attacker code with your privileges. No exploit of a memory bug, no zero-day in a parser. The whole thing rides on a gap between what the approval dialog shows you and what the filesystem does.

The chain, step by step

SymJack is a six-step sequence, and every step looks normal on its own.

1. You clone a repo. A public project, a coding-challenge starter, a "review my PR" request. It contains a few harmless-looking files plus some symlinks you never inspect.

2. The agent reads the project instructions. CLAUDE.md, GEMINI.md, AGENTS.md, whatever your tool auto-ingests on open. These steer the agent toward the next step without you reading them line by line.

3. You accept the workspace-trust prompt. The one you click through a dozen times a day. Trusting the workspace is the gate that lets the agent act.

4. The agent asks to copy files that read as benign. "Copy these sample assets into the project." The destination shown to you is a normal media path. You glance at it and approve.

5. The symlink redirects the write. One of those "media" paths is a symlink pointing at your agent's config files: .mcp.json, settings.json, the directory where tool definitions live. The approval dialog rendered the link's name, not its resolved target. As the write-up put it, the developer approves what the prompt shows, and the kernel writes somewhere else.

6. Restart, and the payload runs. The overwritten config now points at a malicious MCP server. Next time the agent starts, it loads that server and executes the attacker's code at your user privilege level. From there it can read secrets, touch your CI pipeline, and commit code that looks like you wrote it.

Why the approval prompt lies

This is the part that makes SymJack a category problem rather than one vendor's bug. Approval dialogs across these tools display the path string the agent requested. They do not resolve symlinks first. So a request that reads "write to assets/clip.mp4" can land on ~/.config/agent/settings.json if assets/clip.mp4 is a link. You audited the name. The kernel followed the link.

The whole point of an approval prompt is to give a human a true picture of a risky action before it happens. When the picture is the unresolved path, the prompt is giving you confidence without giving you accuracy. That is worse than no prompt, because it trains you to click yes on something you believe you reviewed.

CI is the worst case

The version of this that should keep you up is the non-interactive one. Plenty of teams now run coding agents inside CI: an agent that triages issues, drafts PRs, or auto-fixes failing tests. Those runners commonly auto-trust their own workspace and run the agent in a mode that approves tool calls without a human in the loop.

Drop the payload files and symlinks into a pull request, and the chain executes end to end with nobody watching. No click, no prompt, no operator. The attacker contributes a PR; your CI agent runs it; the agent's config gets rewritten on a machine that holds your deploy keys. This is the same shape as the supply-chain attacks hitting npm right now, except the delivery vehicle is your own automation instead of a dependency.

What to change today

You cannot patch the design assumption yourself, but you can shrink the blast radius a lot.

Treat any host running an agent against untrusted code as compromised-until-proven-otherwise. If you review external PRs or clone strangers' repos with an agent attached, that machine should not also hold production credentials, long-lived tokens, or SSH keys to anything you care about.

Sandbox the workspace. Run agents against untrusted repos inside a container or VM with no host filesystem mount and no ambient cloud credentials. A symlink that escapes the workspace has nowhere useful to land.

Watch your agent config files for changes. .mcp.json, settings.json, and the tool-definition directories should be under version control or file-integrity monitoring. An unexpected diff to one of those is the loudest signal SymJack gives you, and it is the one most people are not looking at.

Turn off blanket auto-approve in CI. If an agent has to run non-interactively, scope what it is allowed to write. Config directories should be read-only to the agent process. A coding agent has no business rewriting its own MCP server list mid-run.

Distrust file-copy approvals that point outside the project. Until the vendors resolve symlinks before showing you the prompt, the only defense at the approval step is to refuse copies whose targets you cannot personally verify. That is a bad place to leave users, which is why the fix has to land in the tools.

What the vendors have to fix

The defensive burden cannot stay on the developer clicking the dialog. The right fixes live in the agents:

Some vendors moved quickly after disclosure. Check your tool's changelog and update. But the structural lesson outlasts any single patch: an AI coding agent's config is executable surface, and anything that can write to it can own your machine.

The bigger pattern

SymJack rhymes with a run of recent attacks that all target the same soft spot: the configuration and instruction files that AI tools read automatically. Backdoored CLAUDE.md files, poisoned MCP servers, malicious VS Code extensions stealing GitHub tokens. The agent's willingness to trust local config is the feature and the wound. As you wire more autonomy into your stack, the files that steer that autonomy become the highest-value thing for an attacker to flip. Guard them like credentials, because functionally that is what they are.