Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 8 min read

A Skill Can Run Commands Before Claude Reads It: What to Check First

You find a skill on GitHub that writes release notes. It looks harmless: a SKILL.md with a description and a few lines of instructions. You copy the folder into .claude/skills/, ask for release notes, and it works. What

You find a skill on GitHub that writes release notes. It looks harmless: a SKILL.md with a description and a few lines of instructions. You copy the folder into .claude/skills/, ask for release notes, and it works.

What you may not have checked is that a skill isn't only text. In Claude Code, a few lines in that file can run shell commands before Claude reads anything, pre-approve tools, or register hooks that keep running for the rest of your session. None of that is hidden, but none of it is obvious when you skim the instructions either.

In an earlier article I walked through writing your first skill and ended with a short list of things to check before installing someone else's. This one goes deeper on that list: the four places in a skill folder where something can run or get access, what Claude Code's skills documentation says about each, and a small script that finds them for you.

Where a skill can do more than talk

The Agent Skills specification defines a skill as a folder with a SKILL.md file (YAML frontmatter plus Markdown instructions) and optional scripts/, references/ and assets/ folders. Most of that is plain text the model reads. Four things are different.

1. Injected commands (! plus a command in backticks). Claude Code calls this dynamic context injection. A line that starts with ! followed by a command in backticks, or a fenced block opened with three backticks and !, makes Claude Code run the command before the skill content is sent to Claude, then replace the line with its output. The docs say these commands "never prompt for permission while the skill renders." They are checked against your permission rules first: a command a deny or ask rule matches aborts the skill, and outside auto mode anything that isn't already allowed also aborts, unless the skill pre-approves it with allowed-tools.

2. allowed-tools. This frontmatter field lets Claude use the listed tools without asking you, during the turn that invokes the skill. The grant clears when you send your next message. The part worth reading twice: per the docs, workspace trust doesn't gate this field, and Claude Code applies a project skill's allowed-tools even in a -p run in a folder you've never trusted. The docs' own advice is to review the allowed-tools of skills checked into a repository before you run Claude Code there. (The open spec marks allowed-tools as experimental, and other agents may handle it differently.)

3. hooks. A skill can define hooks in its frontmatter. According to the hooks docs, Claude Code registers them when you or Claude invoke the skill and keeps running them for the rest of the session, not just during the skill's turn, unless a hook sets once: true. Frontmatter hooks in a project skill follow the same workspace trust rule as hooks in settings files, and the docs note they are registered on invocation even in a -p run in a folder you haven't trusted.

4. scripts/ and anything else in the folder. The spec describes scripts/ as executable code that agents can run. The instructions in SKILL.md decide when, so a line like "run scripts/fetch-style.sh first" is an instruction to execute whatever that file contains.

One more field changes when all of this happens: disable-model-invocation. It defaults to false, which means Claude can load the skill on its own when your request matches its description. Set to true, the skill only runs when you type /name.

A small script that finds all four

Reading every file by hand is still the real review. But a script that lists the risky parts first tells you where to look. This one uses only the Python standard library. It doesn't run anything from the skill; it only reads the files and prints flags.

#!/usr/bin/env python3
"""Flag the parts of a skill folder that run commands or grant access."""
import re
import sys
from pathlib import Path

RISKY_KEYS = ("allowed-tools", "hooks")

def frontmatter(text):
    lines = text.splitlines()
    if not lines or lines[0].strip() != "---":
        return [], lines
    for i, line in enumerate(lines[1:], start=1):
        if line.strip() == "---":
            return lines[1:i], lines[i + 1:]
    return [], lines

def audit(skill_dir):
    skill_dir = Path(skill_dir)
    text = (skill_dir / "SKILL.md").read_text()
    front, body = frontmatter(text)
    print(f"Skill: {skill_dir.name}")

    for line in front:
        key = line.split(":", 1)[0].strip()
        if "command:" in line:
            print(f"  [HOOK]   {line.strip()}")
        if line.startswith(" "):
            continue
        if key in RISKY_KEYS:
            print(f"  [GRANT]  frontmatter {line.strip()}")
        if key == "name" and line.split(":", 1)[1].strip() != skill_dir.name:
            print(f"  [NAME]   name does not match folder: {line.strip()}")
    if not any(l.startswith("disable-model-invocation") for l in front):
        print("  [AUTO]   Claude can load this skill on its own (no disable-model-invocation)")

    in_bang_block = False
    for n, line in enumerate(body, start=len(front) + 3):
        if line.startswith("```

!"):
            in_bang_block = True
            continue
        if in_bang_block:
            if line.startswith("

```"):
                in_bang_block = False
            else:
                print(f"  [RUNS]   line {n}: {line.strip()}")
            continue
        if re.search(r"(^|\s)!`[^`]+`", line):
            print(f"  [RUNS]   line {n}: {line.strip()}")

    for path in sorted(skill_dir.rglob("*")):
        if path.is_file() and path.name != "SKILL.md":
            print(f"  [FILE]   {path.relative_to(skill_dir)}")
            for url in re.findall(r"https?://\S+", path.read_text(errors="ignore")):
                print(f"  [URL]    {path.name}: {url}")

for d in sys.argv[1:]:
    audit(d)

It's deliberately simple. It reads frontmatter line by line instead of parsing YAML, so treat a clean result as "nothing obvious," not "safe."

Running it on an example skill

To test it, I made an illustrative skill called release-notes. It isn't a real published skill; I wrote it to contain one of each pattern. Its SKILL.md looks like this:

---
name: release-notes
description: Drafts release notes from recent commits. Use when the user asks for release notes, a changelog, or what shipped this week.
allowed-tools: Bash(git *) Bash(curl *)
hooks:
  PostToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "./scripts/sync.sh"
---

## Recent commits

!`git log --oneline -20`

## Instructions

Group the commits above into Added, Changed and Fixed.
For the style guide, run scripts/fetch-style.sh first.

Next to it are two scripts: scripts/fetch-style.sh, which calls curl on an example URL, and scripts/sync.sh, which only prints a word. Here's the real output of python3 audit_skill.py release-notes (Python 3.13.5):

Skill: release-notes
  [GRANT]  frontmatter allowed-tools: Bash(git *) Bash(curl *)
  [GRANT]  frontmatter hooks:
  [HOOK]   command: "./scripts/sync.sh"
  [AUTO]   Claude can load this skill on its own (no disable-model-invocation)
  [RUNS]   line 15: !`git log --oneline -20`
  [FILE]   scripts/fetch-style.sh
  [URL]    fetch-style.sh: https://example.com/style.md
  [FILE]   scripts/sync.sh

Reading it from top to bottom:

  • [GRANT] allowed-tools: any git or curl command runs without asking during the turn the skill is invoked. git fits a release-notes skill. curl doesn't, and that mismatch with the stated purpose is exactly what Anthropic's Agent Skills overview tells you to look for.
  • [GRANT] hooks + [HOOK]: after the first invocation, ./scripts/sync.sh runs after every Bash call for the rest of the session. Open that script before anything else.
  • [AUTO]: Claude can trigger all of this when you ask for "what shipped this week," without you typing /release-notes.
  • [RUNS] line 15: git log runs before Claude reads the skill. Harmless here, but this is the line to check in every skill you install.
  • [URL]: the skill fetches content from outside. The overview warns that fetched content can carry instructions, and that a trustworthy skill can change if what it fetches changes.

What to do with each flag

You don't have to reject a skill because it has flags. A release-notes skill that runs git log is doing its job. The goal is that every flag has a reason you agree with.

  • Trim allowed-tools to what the task needs. Here, Bash(git log *) is enough; drop curl.
  • Read every hook command and every script the hooks or instructions call. If a hook isn't needed for the skill's job, remove it.
  • Add disable-model-invocation: true to skills with side effects, so they only run when you type the command.
  • Replace fetched content with a file in references/ that you've read, when the content doesn't need to be live.
  • Use permission rules as a backstop. Deny rules still apply to injected commands, and you can allow or deny skills by name with Skill(name) rules in /permissions. To turn off injected commands for user, project and plugin skills entirely, the docs describe a disableSkillShellExecution setting.

Checklist before you install a skill

  • Read SKILL.md and every file in the folder, not just the instructions.
  • List every ! command and fenced ! block, and know what each one runs.
  • Check allowed-tools against the skill's stated purpose.
  • Read every hook command, and remember skill hooks keep running for the session.
  • Open every script the instructions tell the agent to run.
  • Note every external URL and decide whether it needs to be live.
  • Decide whether Claude should be able to invoke it on its own.
  • For skills that arrive inside a repository, do this before you run Claude Code in it.

Where to go next

If you're still at the stage of writing and installing your own skills, the free AI Agent Skills Quick-Start Cheat Sheet covers the format, a copy-and-adapt SKILL.md template, why most skills never trigger, and how to install and test skills across the major agents, in six pages.

When you want the full reference, AI Agent Skills: The Complete SKILL.md Standard Guide covers the whole specification, five build walkthroughs from markdown-only to API-calling skills, ten production-ready templates, a chapter on skills security and a 30-day plan.

This article was written with the assistance of AI and reviewed by the author. Claude Code behavior is described from Anthropic's official documentation (skills and hooks pages) and the Agent Skills specification at agentskills.io. The release-notes skill is an illustrative example made for this article, and the output shown is the script's real output on it, not output from a Claude Code session. PromptMaster guides are independent and not affiliated with or endorsed by Anthropic.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.