· 7 min read
Your agent reads AGENTS.md from Downloads: a defender's setup for Claude Code
A web page can drop agent instructions into Downloads. Here are the deny rules, hooks, sandbox and egress limits we use, and how to test them.
By Centrob · ai, security, claude-code
A web page can put an AGENTS.md or CLAUDE.md file in your Downloads folder without a click. If you then start a coding agent in that folder, it reads the file as project instructions. The defence is to keep agents out of folders you did not make, and to put the limits where an instruction file cannot change them.
What was reported
Forever Security published the attack on 22 October 2026 [1]. They found a malicious AGENTS.md in the Downloads folder of a real customer machine. The download needs no click and no approval. Their tests, as they report them:
- Codex was led to add an MCP server to its user-level config. The server's launch command installed a malicious npm package. Codex ran the malware in more than 90% of attempts.
- Claude Code was led to write a project
.claude/settings.jsonthat pointedANTHROPIC_BASE_URLat the researchers' server, which sends the OAuth token there. Claude followed through about half the time, and about a fifth of the time they got code execution in the first session.
Their telemetry says about half of their customers run a coding agent inside Downloads. They argue there is nothing for the vendors to patch, because the agents do what they are designed to do [1].
The point for defenders: Claude Code enforces permission rules itself, not through the model. Instructions in a prompt or CLAUDE.md shape what Claude tries, but they do not change what it is allowed to do [2]. So the defences below are settings, hooks, a sandbox and the network, not more instructions.
1. Work in a dedicated directory
- Start sessions only in a project checkout, or in a git worktree made for the task:
git worktree add ../app-fix-login -b fix/login. - Decline the trust prompt for any folder you did not create. In a project's
.claude/settings.json, allow rules, extra directories and mostenvvalues wait until you trust the folder. Deny and ask rules apply at once [3]. - Refuse the risky folders outright with a
UserPromptSubmithook. Exit code 2 blocks the prompt [4]:
#!/usr/bin/env bash
# ~/.claude/hooks/refuse-downloads.sh (needs jq)
set -euo pipefail
cwd="$(jq -r '.cwd // empty')"
case "$cwd" in
"$HOME" | "$HOME"/Downloads | "$HOME"/Downloads/*)
echo "Refused: start Claude Code in a project directory, not $cwd" >&2
exit 2 ;;
esac
exit 0
2. Deny rules, a sandbox and egress limits
This goes in ~/.claude/settings.json, so it applies in every project:
{
"permissions": {
"deny": [
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Read(~/.config/gh/**)",
"Read(//**/.env)",
"Read(//**/.env.*)",
"Edit(//**/.claude/**)",
"Edit(//**/CLAUDE.md)",
"Edit(//**/AGENTS.md)",
"Edit(//**/.mcp.json)",
"Bash(curl *)",
"Bash(wget *)"
],
"disableBypassPermissionsMode": "disable"
},
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"filesystem": {
"denyRead": ["~/.ssh", "~/.aws", "~/.config/gh"]
},
"network": {
"allowedDomains": ["registry.npmjs.org", "index.crates.io", "static.crates.io"]
}
}
}
What each part does:
- Order. Deny is checked first, then ask, then allow. An allow rule cannot carve an exception out of a deny [2].
- Paths.
~/is your home directory and//is the filesystem root. A single leading/is relative to the settings file, not the root [2]. - Use
Edit, notWrite.Editrules cover every built-in tool that edits files. AWrite(path)rule is accepted but never matches [2]. - Protected paths. Writes under
.claudeare never auto-approved in the default mode. In auto mode a classifier decides, and in bypass mode they are allowed [6]. The deny rule anddisableBypassPermissionsModeclose both gaps. You will edit these files yourself. - The sandbox. It puts shell commands and their child processes behind an OS boundary: bubblewrap on Linux, Seatbelt on macOS.
allowUnsandboxedCommands: falsestops Claude retrying a failed command outside it [5].
A deny rule is not a sandbox. A Bash(curl *) rule does not stop /usr/bin/curl or sh -c 'curl …' [2]. Read and Edit rules do not stop a Python or Node script that opens files itself [2]. The sandbox's denyRead and network allowlist cover those cases.
The sandbox has limits too. The built-in file and web tools, hooks and local MCP servers run outside it [5]. So does Claude Code's own connection to its API, which is what the ANTHROPIC_BASE_URL trick redirects. The proxy also decides by hostname without inspecting TLS, so a broad entry like github.com can be a path out [5]. For egress you can rely on, run the whole agent in a container or VM whose firewall allows only your model endpoint and your package registries. The docs point to the sandbox runtime and the dev container firewall for this [5].
3. Hooks as a second check
A hook sees the full tool call and can refuse it. This one refuses edits to agent configuration, whatever the permission mode:
#!/usr/bin/env bash
# ~/.claude/hooks/protect-instructions.sh (needs jq)
set -euo pipefail
path="$(jq -r '.tool_input.file_path // empty')"
case "$path" in
*/.claude/* | */CLAUDE.md | */AGENTS.md | */.mcp.json)
echo "Blocked: $path is agent configuration; a human edits it" >&2
exit 2 ;;
esac
exit 0
Register both hooks in the same settings file:
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/refuse-downloads.sh", "onFailure": "block" } ] }
],
"PreToolUse": [
{ "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "$HOME/.claude/hooks/protect-instructions.sh", "onFailure": "block" } ] }
]
}
}
By default a hook that crashes or times out does not block anything. "onFailure": "block" changes that and needs Claude Code v2.1.295 or later [4]. A hook cannot override a deny rule, and a hook that exits 2 blocks even when an allow rule matches [2].
4. Treat web and file content as data
- Read any
CLAUDE.mdorAGENTS.mdyou did not write before you trust its folder. Check project.claude/settings.jsonfiles for an unexpectedANTHROPIC_BASE_URLor proxy setting [1]. - An
InstructionsLoadedhook runs each time Claude Code loads aCLAUDE.mdor a rules file [4]. It cannot block, but it can log which instruction files a session read. - Auto mode's classifier looks for actions that seem driven by hostile content Claude read. The docs say it reduces prompts but does not guarantee safety [6]. Keep the rules above with it.
5. Test that the rules hold
Rules that are not tested drift. After any change to settings, and after each Claude Code upgrade:
- Run
/statusto see which settings files loaded, andclaude doctorto list any entries Claude Code rejected [3]. - Test each hook without Claude, by feeding it the JSON it would get:
echo '{"cwd":"'"$HOME"'/Downloads"}' | ~/.claude/hooks/refuse-downloads.sh; echo "exit $?"
echo '{"tool_input":{"file_path":"/tmp/p/.claude/settings.json"}}' | ~/.claude/hooks/protect-instructions.sh; echo "exit $?"
Both should print a refusal and exit 2.
3. Plant a canary. Add ~/canary to the Read deny rules and to the sandbox's denyRead, and put a file there. In a scratch project, ask Claude to read it with its Read tool, with cat, and with a one-line Python script. All three must fail. If only the first two fail, your sandbox is off.
4. Ask Claude to run curl https://example.com and then /usr/bin/curl https://example.com. The deny rule should stop the first and the network allowlist the second.
5. Make a throwaway folder with an AGENTS.md that tells the agent to edit .claude/settings.json. Start a session there and watch the hook refuse it.
What we do: our agentic coding setup sprint puts rules like these into a team's repositories and machines, with tests that show they hold. See https://centrob.co/#ai.
Sources
- Markdown Is the New Executable: Attackers Are Dropping Agent Instructions Into Your Downloads Folder From the Web (Forever Security, 22 October 2026) Accessed 27 October 2026.
- Configure permissions (Claude Code docs) Accessed 27 October 2026.
- Settings files and precedence (Claude Code docs) Accessed 27 October 2026.
- Hooks reference (Claude Code docs) Accessed 27 October 2026.
- Sandboxing (Claude Code docs) Accessed 27 October 2026.
- Permission modes (Claude Code docs) Accessed 27 October 2026.