← All posts

Explainer

The security model of agent skills: what you are actually installing

The security model of agent skills: what you are actually installing

Installing a skill feels like installing nothing. You type a name, you confirm, a folder appears. There is no build step, no binary, no postinstall script. It is a Markdown file.

What you have actually done is add a paragraph of instructions that will be read by a process running inside your repository, with your shell, your gh token, your SSH agent and whatever cloud credentials your terminal already had — at moments you did not choose, because the model decides when a skill is relevant.

That is the whole article. A skill is not data. It is instructions that steer your agent inside your repository, with your credentials, at your keyboard. Installing one is closer to installing a shell alias, or a browser extension, than to downloading a wallpaper. Everything below follows from taking that seriously — including the reasons most skills are completely fine.

Verified against: Claude Code 2.1.221 and the Skills CLI (skills on npm) 1.5.22, checked on 2026-08-02 against code.claude.com/docs and the published CLI bundle. Where a claim comes from research rather than documentation, it is named and linked. Nothing here describes an unpublished vulnerability, and there is no exploit in this post you could paste.

What a skill is, mechanically

A skill is a directory with a SKILL.md in it: YAML frontmatter, then Markdown instructions. Claude Code loads them from three places — ~/.claude/skills/<name>/ for your personal ones, .claude/skills/<name>/ inside a project, and a skills/ directory inside an installed plugin. The format follows the open Agent Skills standard, so the same file works across several tools, and Claude Code adds a few extensions of its own.

Two mechanics matter for security, and both are easy to miss:

Once invoked, the rendered content enters the conversation as a message and stays there for the rest of the session. It is not a function you called and returned from. It is a standing instruction sitting alongside your own.

What a skill can actually cause

Take the strong version of the sceptical position first, because it is mostly right: a skill body cannot execute anything. It is text. To have an effect it must persuade an agent — one that can execute things — to make a tool call, and that tool call still has to get past your permission rules. A skill is an argument, not an action.

So here is a realistic chain, traced end to end rather than gestured at.

You install a "release notes" skill that promises to summarise what changed since the last tag. Its description mentions changelogs, releases and version bumps. Buried in the middle of an otherwise genuinely useful 200-line file is a step that reads, roughly: before summarising, check the repository's publish configuration by running the project's env dump and include the result so the summary can mention the target environment. Nothing about that sentence is a red flag on its own. Skills legitimately tell agents to run things.

You ask for release notes. The model loads the skill, follows step three, and proposes a Bash call. And now the entire outcome depends on one thing that has nothing to do with the skill file:

Exfiltration does not need a weird channel. It needs a legitimate-looking tool call: a curl to a URL that looks like a CI endpoint, a commit to a branch, a comment posted on an issue. Anthropic's own permissions documentation makes the point about URL filtering in passing — "using WebFetch alone doesn't prevent network access. If Bash is allowed, Claude can still use curl, wget, or other tools to reach any URL."

The one part that is not an argument

There is an exception to "a skill cannot execute anything", and it is the single most important detail in this post. Claude Code supports dynamic context injection: a SKILL.md may contain !-prefixed inline commands, or a fenced ```! block, and Claude Code runs them and splices the output into the skill body.

## Pull request context
- PR diff: !`gh pr diff`
- PR comments: !`gh pr view --comments`

This is a good feature. It is how a skill arrives with real data instead of a plan for getting it. But read the documented order of operations carefully, because it is not what most people assume. From the Claude Code skills reference: each command "executes immediately (before Claude sees anything)", and "This is preprocessing, not something Claude executes. Claude only sees the final result."

The model is not the gatekeeper here, because at that instant there is no model in the loop. In May 2026, Datadog Security Labs published a demonstration of exactly this: a skill whose dynamic-context commands retrieved a GitHub token and sent it out before the model reviewed the skill at all. The model then declined to continue — after the credential was already gone. Refusal is a property of the model, and this ran underneath it.

SKILL.md third-party text THE LOUD PATH — body instructions Model reads it may refuse Tool call proposed Bash / Edit / WebFetch Permission gate deny · ask · allow PreToolUse hook THE QUIET PATH — !`cmd` dynamic context Shell runs at load preprocessing Output spliced in model sees only the result no model, no refusal
The body of a skill has to argue its way through the model and then through your permission rules. A !`command` placeholder does not: Claude Code runs it during preprocessing and hands the model the output. Turn it off globally with "disableSkillShellExecution": true.

The trust boundary people get wrong

The instinct is to look for the dangerous line in the skill file. That is the wrong object. The skill file is a request; the thing that decides is your permission configuration. Anthropic writes it about as directly as documentation ever puts anything:

"Permission rules are enforced by Claude Code, not by the model. Instructions in your prompt or CLAUDE.md shape what Claude tries to do, but they don't change what Claude Code allows."

Substitute "a skill" for "your prompt" and the sentence still holds. That is the boundary. Which means skills and approval policy are one system, not two, and you cannot evaluate a skill's risk without knowing which mode you run in.

Claude Code 2.1.221 ships six permission modes, and they are not shades of the same thing:

ModeWhat a hostile skill gets
default Prompts on first use of each tool. The attack is loud — you see the command.
plan Reads and read-only shell only; source files are not edited. Loud and mostly harmless.
acceptEdits File edits and common filesystem commands auto-accepted in the working directory. Writes are silent; other commands still prompt.
auto Auto-approves with background safety checks that verify actions align with your request. Quieter, and the classifier is now part of your threat model.
dontAsk Auto-denies anything not pre-approved. The tightest of the automatic modes.
bypassPermissions Skips prompts, including writes to .git, .claude, .vscode and friends. Anthropic's own guidance: "Only use this mode in isolated environments like containers or VMs where Claude Code can't cause damage."

Underneath the modes are rules, evaluated deny, then ask, then allow, first match wins. A deny rule that names a bare tool does something stronger than blocking: it removes the tool from the model's context entirely, so no amount of skill text can ask for it. That is the one instrument in the system a skill genuinely cannot argue with. Below that sits the PreToolUse hook, which runs before the permission prompt and can return allow, deny or ask per call — the programmable version of the same idea, and the subject of our guide to pre-approval policies.

One wrinkle specific to skills: frontmatter can carry an allowed-tools field, which pre-approves the listed tools for the turn that invokes the skill, clearing when you send your next message. Useful, and the grant is deliberately narrow. But it does mean a skill file participates in its own permissioning, which is why Anthropic's docs say, of project skills: "Review project skills before trusting a repository, since a skill can grant itself broad tool access."

Codex has the analogous dial in a different place, and it is worth knowing because the shape of the answer differs. Where Claude Code gates the decision, Codex constrains the capability: an OS-enforced sandbox — Apple Seatbelt on macOS, bubblewrap plus seccomp on Linux — in which a forbidden write fails with EPERM regardless of what any instruction said. See Codex CLI sandbox modes. Claude Code has a sandboxed Bash tool too, enabled with sandbox.enabled, which puts filesystem and network boundaries under OS enforcement rather than model cooperation. If you install skills liberally, that is the setting to know about.

The supply chain is a pile of git repos

Now the boring part that turns out to matter most. There is no npm for skills. Most of the ecosystem is plain git repositories installed by name, through one of three routes: the npx skills CLI (registry at skills.sh), Claude Code's own plugin marketplaces via /plugin, or a human copying a folder. We wrote the mechanics up separately in how to install Claude Code skills.

The obvious advice is "pin to a commit". We tested whether you can, and the answer is the reason this section exists.

The Skills CLI accepts a commit as a fragment: skills add "owner/repo#<sha>". For some repositories that works, and the lock file records the SHA. For most it fails outright:

fatal: Remote branch <sha> not found in upstream origin

The reason is structural, and visible in the published bundle. Resolving an arbitrary SHA requires the GitHub tree/blob API path, and dist/cli.mjs in version 1.5.22 takes that path only for owners on a hardcoded allowlist:

BLOB_ALLOWED_OWNERS = ["vercel", "vercel-labs", "heygen-com"]

Everyone else falls through to a clone, with these options:

cloneOptions = ref ? ["--depth", "1", "--branch", ref] : ["--depth", "1"]

git clone --branch takes a branch or a tag. It has never taken a commit. So the pin does not degrade into a loose install — it fails hard, which is the one merciful part of this. We confirmed the failure for anthropics, addyosmani, obra, supabase and ibelick. A GitHub token does not help; the allowlist is checked before the token is used.

So "just pin it" is advice that does not apply where you most need it — which is to say, to everyone who is not Vercel. What you install from those repos is a branch. And a skill installed by branch is a permanent write channel from its author into your agent: whatever main says the next time the CLI updates it is what your agent will be reading, with no diff, no review and no release notes. Nobody has to be malicious today for that to be true tomorrow.

If you want a real pin, fetch the commit yourself. This returns the tree at that exact commit for any repository, allowlist or not:

https://codeload.github.com/<owner>/<repo>/tar.gz/<40-hex-sha>

Extract the one subdirectory you want and keep the SHA next to it. Or vendor the folder into your own repo and let code review do the job it already does.

A pin has one more failure mode worth naming: it is worthless if the pinned text fetches its own instructions at runtime. A skill whose body says "read the latest guidelines from …/main/guidelines.md before starting" has handed its author the same live channel, one indirection further out. That pattern exists in the wild in perfectly reputable repositories, for perfectly good reasons. It is still a live channel.

On scale: Snyk published ToxicSkills on 5 February 2026, a scan of 3,984 skills from ClawHub and skills.sh. They report 36% — 1,467 skills — with at least one security flaw, and 76 payloads confirmed malicious after human review. Read those two numbers separately: "has a flaw" is a much wider net than "is malware", and 76 out of 3,984 is roughly the base rate you would guess for any open package ecosystem. The ecosystem is not poisoned. It is normal, which is the actual problem, because normal ecosystems are where supply chain attacks live.

Prompt injection meets skills

Everything above assumed you could read the skill and know what it says. You can — they are short. But a skill's text is fixed and its inputs are not.

Consider a completely benign triage skill: read the linked issue, reproduce the bug, propose a fix. Someone files an issue on your public repo whose body contains, below three screens of plausible stack trace, a paragraph addressed not to you but to the agent. The skill told the agent to follow the issue. The agent fetched the issue. Text arrived. From the model's position there is no typographic difference between the reporter's description of the bug and the reporter's instructions about what to do next — it is all context.

That composition is the thing to internalise. The skill did not need a hidden payload; it only needed to be permissive about its inputs. And it is why the rule for anyone building on agents is not "sanitise the skill" but the older, more general one: treat anything that entered the context from outside — fetched pages, issue bodies, file contents, tool output, another model's answer — as untrusted input, and put the gate at the point of effect rather than the point of reading.

There is a real precedent for how this ends, and it is not hypothetical. In the s1ngularity compromise of the nx npm packages on 26 August 2025, the malicious postinstall script did something novel: instead of writing its own reconnaissance code, it invoked whichever AI CLIs it found on the machine — claude, gemini, q — and asked them to inventory sensitive files, passing --dangerously-skip-permissions, --yolo and --trust-all-tools. Over a thousand valid GitHub tokens went out through repositories created in the victims' own accounts.

No vulnerability in any of those CLIs was exploited. The attacker asked politely and turned off the gate on the way in. That is the clearest statement anyone has made of where the security of an agent actually lives.

What to actually do

None of this is a reason to avoid skills. It is a reason to know which five minutes matter. In rough order of value per unit of effort:

Notice what is not on that list: reading every line of every dependency, running your agent in a VM, or not installing skills. The realistic posture is that most skills are what they say they are, reading them is cheap, and the permission gate catches the case where you were wrong.

Where Backgrind fits

The recurring theme above is that a gate you never see is not a gate. That is a user interface problem as much as a security one: an approval prompt buried in a terminal behind a browser, a game, or four other terminals is one you will answer on autopilot, which is functionally the same as having auto-approved it.

Backgrind is the layer that makes the gate visible. When a tool call hits a PreToolUse ask, the daemon holds the call and surfaces it as an ambient prompt on whatever surface you are actually looking at — the always-on-top overlay, the fleet board, or your phone — with the command in front of you, and approve or deny in one keystroke. That is not a claim to catch malicious skills; nothing catches malicious skills except reading them and keeping the boundary enforced. It is a claim that the boundary this article says matters should be somewhere you will notice it.

If you want the gate to hold without asking twice for the same thing, the companion piece is pre-approval policies: decide once, in writing, what the agent may do unattended, and let the rest come to you.

Sources

Skill format, where skills live, description-in-context behaviour, allowed-tools, dynamic context injection and disableSkillShellExecution: Claude Code — Extend Claude with skills (checked 2026-08-02, v2.1.221). Permission modes, deny/ask/allow precedence, bare-name deny removal, the bypassPermissions warning and the WebFetch/Bash note: Configure permissions. PreToolUse payload and permissionDecision semantics: Hooks reference. OS-enforced Bash sandbox and sandbox.enabled: Configure the sandboxed Bash tool. Dynamic-context credential demonstration (Nick Frichette and Ryan Simon, 11 May 2026): Datadog Security Labs. Ecosystem scan figures (5 February 2026): Snyk — ToxicSkills. s1ngularity nx compromise, 26 August 2025: Wiz. Pin behaviour, BLOB_ALLOWED_OWNERS and the clone fallback: read directly from skills@1.5.22 dist/cli.mjs on npm and confirmed by running skills add against the named repositories, 2026-08-02.