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

> Source: <https://dev.to/promptmaster/a-skill-can-run-commands-before-claude-reads-it-what-to-check-first-20b5>
> Published: 2026-11-14 10:42:05+00:00

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](https://dev.to/promptmaster/agent-skills-are-now-an-open-standard-write-your-first-skillmd-in-15-minutes-21g0) 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](https://code.claude.com/docs/en/skills) says about each, and a small script that finds them for you.

The [Agent Skills specification](https://agentskills.io/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](https://code.claude.com/docs/en/hooks), 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`.

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.

``` bash
#!/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."

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`` git` or `curl` command runs without asking during the turn the skill is invoked. `[GRANT] hooks` + `[HOOK]``./scripts/sync.sh` runs after every Bash call for the rest of the session. Open that script before anything else.`[AUTO]``/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]`
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.

`allowed-tools`` Bash(git log *)` is enough; drop `disable-model-invocation: true`` references/` that you've read, when the content doesn't need to be live.`Skill(name)` rules in `/permissions`. To turn off injected commands for user, project and plugin skills entirely, the docs describe a `disableSkillShellExecution` setting.`SKILL.md` and every file in the folder, not just the instructions.`!` command and fenced If you're still at the stage of writing and installing your own skills, the free [AI Agent Skills Quick-Start Cheat Sheet](https://promptmasterstore.gumroad.com/l/ai-agent-skills-cheat-sheet?utm_source=devto) 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](https://promptmasterstore.gumroad.com/l/ai-agent-skills-complete-guide?utm_source=devto) 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.*
