Explainer
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:
- Descriptions are always in context. The body of a skill loads only when it is
invoked, but its
descriptionsits in the model's context from the start of the session so it knows what is available. That is what makes skills cheap. It is also what makes them ambient: a skill you installed for one job can be pulled in by the model at a moment you were not thinking about it, because the description matched. - The model usually decides when to load one. Unless the author set
disable-model-invocation: true, invocation is the model's call, not yours. You are not choosing to run this text; you are making it eligible to run.
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:
- If you have an approval gate, you see a prompt with the exact command in it. The attack is loud. You cancel, you read the file properly, you uninstall it.
- If you run with
Bashbroadly allowed, or inbypassPermissions, the command runs, its output is now in the transcript, and the next step of the skill — "post the summary to the release webhook" — has something to carry. No prompt fires, because you already answered the question in advance.
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.
!`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:
| Mode | What 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:
- Read the
SKILL.mdbefore installing. This is realistic advice in a way that "audit your dependencies" never was — a skill is one Markdown file, usually under 200 lines, written in English. You can read it in the time it takes a package to install. Read the frontmatter first:allowed-toolstells you what it pre-approves for itself, anddescriptiontells you when it will fire unasked. - Search the file for
!backtick placeholders and```!blocks before anything else. Those are the lines that run before the model gets a vote. If you never want that behaviour, set"disableSkillShellExecution": truein your settings; each command is then replaced with[shell command execution disabled by policy]. - Look for URLs in the body. A skill that fetches its own instructions at runtime cannot be pinned in any meaningful sense, whatever the lock file says.
- Prefer sources that pin — and check that the pin took. If the CLI cannot pin
your repo, fetch the tarball at a commit or vendor the folder. Treat "installed from
main" as a subscription, not a purchase. - Keep the gate on for anything that writes, publishes or spends. Read-only work
can run wide open;
git push,npm publish, deploys and anything touching a billing API should reach you. Deny rules on~/.ssh,~/.awsand your credential files cost nothing — the agent does not need them to write code. If you administer other people's machines,permissions.disableBypassPermissionsModein managed settings is the one that stops the accident. - Scope project skills to the project. A skill in a repo's
.claude/skills/is code that ships with the repo and should be reviewed at PR time like any other code — especially since accepting the workspace trust dialog is what activates itsallowed-tools. Do not put something in~/.claude/skills/that only one project needs; personal skills follow you into every repository you open, including the ones you cloned to take a look at. - Know how to uninstall before you install. A personal skill is a directory —
delete it and Claude Code notices within the session. A CLI-installed one comes off with
skills remove -g -s <name> -a <agents> -y. A plugin skill is managed through/plugin. Three routes in means three routes out, and knowing which one you used is part of installing safely.
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.