intermediate devsecops · 45 minutes ·

Claude Code Security Skills: A DevSecOps Tutorial

Build Claude Code security skills that scan for secrets, run SAST, and catch prompt injection, then enforce them with hooks in your DevSecOps pipeline.

Prerequisites

  • A working Claude Code install
  • A Git repository you can run scans against
  • Basic YAML and shell familiarity

Tools Used

Claude CodeSemgrepGitleaksGitHub Actions
Claude Code Security Skills: A DevSecOps Tutorial

A Claude Code security skill is a folder with a SKILL.md file that teaches the agent one security job: scan a diff for secrets, run SAST across changed files, or probe a prompt for injection. You drop it in .claude/skills/, describe when it should fire, and the agent loads it on demand. The payoff for a DevSecOps team is that security checks stop being a separate tool you remember to run and become something the coding agent does inline, on every change, with the same context it used to write the code.

This tutorial builds three security skills from scratch, wires one of them to a real scanner over MCP, and then locks the whole thing down with a hook so the agent cannot merge past a failing scan. Everything here runs locally first, then in a GitHub Actions job. By the end you will have a .claude/ directory you can commit and share across your team.

Why skills instead of a wrapper script

You could write a Bash script that shells out to a scanner and call it from CI. Plenty of teams do. The difference with a skill is that the agent decides when to run it and reads the output as context, not as a wall of log text a human has to triage. When Claude Code edits an authentication handler and a secret-scanning skill is present, the agent can run the scan, see the finding, and fix the leak before it ever writes the file. That feedback loop is tighter than a CI failure three minutes after you push.

Skills are also composable. A single security skill stays small and focused. You build a library of them, and the agent picks the right one for the task. That mirrors how a good security team works: narrow, well-understood checks rather than one giant do-everything scanner.

There is a catch, and it is the reason this tutorial spends real time on hooks and skill auditing. A skill is executable instruction text. A malicious or sloppy SKILL.md can tell the agent to do the wrong thing, and a skill you installed from a public registry is supply-chain risk like any other dependency. We cover that in the auditing section, and it pairs well with our writeup on detecting malicious AI agent skills.

Prerequisites and layout

You need a working Claude Code install, a Git repo, and Node.js 22.12 or newer for the tooling. Python 3.12 helps if you want to run the scanners locally outside a container. Create the skills directory at the root of your repo:

$ mkdir -p .claude/skills
$ cd .claude/skills

Every skill lives in its own subfolder, and the folder must contain a SKILL.md with YAML frontmatter. The two required frontmatter fields are name and description. The description is the most important line you will write, because that is the text the agent reads to decide whether the skill applies to the current task. Vague descriptions never fire; specific ones do.

Here is the target structure once all three skills exist:

.claude/
  skills/
    secret-scan/
      SKILL.md
    sast-scan/
      SKILL.md
    prompt-injection-check/
      SKILL.md
  settings.json

Step 1: a secret-scanning skill

Secret scanning is the highest-value check to automate first, because a leaked credential is an immediate, exploitable incident rather than a latent bug. We use Gitleaks as the engine because it runs as a single static binary with no daemon, which makes it easy for the agent to invoke and easy to pin in CI.

Create .claude/skills/secret-scan/SKILL.md:

---
name: secret-scan
description: >
  Scan staged changes or a target path for hardcoded secrets (API keys,
  tokens, private keys, connection strings) using gitleaks. Use before
  committing, when reviewing a diff, or when the user mentions credentials,
  secrets, tokens, or .env files.
---

# Secret scanning

When this skill fires, run gitleaks against the working tree and report
every finding with its file, line, and rule ID. Never print the secret
value itself in full; redact all but the last four characters.

## How to run

1. Confirm gitleaks is installed: `gitleaks version`.
2. Scan the staged diff first, since that is what is about to be committed:
   `gitleaks protect --staged --redact --report-format json`.
3. If nothing is staged, scan the full tree:
   `gitleaks detect --redact --report-format json`.
4. Parse the JSON report. For each finding, show file, line, and rule.
5. If findings exist, stop and tell the user which credential leaked and
   where. Suggest rotating the secret, not just deleting the line, because
   the value is already in Git history.

## Rules

- A finding is a hard stop. Do not proceed with a commit that has one.
- Treat .env.example and test fixtures as expected; flag them low priority.
- If gitleaks is missing, say so and do not silently skip the check.

The instruction to redact and to recommend rotation matters. A common failure mode is an agent that “fixes” a leak by deleting the offending line, which does nothing, because the secret is already in the commit history and, if it was ever pushed, likely already scraped. The skill encodes the correct response so you do not have to remember it at 2 a.m.

Test it by asking Claude Code to review a branch that has a planted fake key. The agent loads the skill from the description match, runs Gitleaks, and reports the finding with the rule ID. If you want a second layer at the pipeline level rather than the editor, our guide on stopping secret leaks in CI/CD covers the GitHub Actions side.

Step 2: a SAST skill wired to Semgrep over MCP

Static analysis is a poor fit for a shell-out skill, because good SAST needs a rules engine and structured output, not grep. The clean approach is to run Semgrep as an MCP server and let the skill call its tools. Semgrep ships an MCP server that exposes its scanning as deterministic tools the agent can call, backed by a large community rule set.

First register the MCP server in your project. Add it to .claude/settings.json (create the file if it does not exist):

{
  "mcpServers": {
    "semgrep": {
      "command": "uvx",
      "args": ["semgrep-mcp"]
    }
  }
}

Then create .claude/skills/sast-scan/SKILL.md:

---
name: sast-scan
description: >
  Run static application security testing (SAST) on changed source files
  using the semgrep MCP server. Use when code is edited in a security
  sensitive area (auth, crypto, input handling, SQL, file I/O, subprocess),
  or when the user asks for a security review of a diff.
---

# SAST review

Use the semgrep MCP tools to scan the files that changed in this session.
Do not scan the whole repository on every run; scope to the diff so the
review stays fast and relevant.

## Procedure

1. List the files changed in the working tree.
2. Call the semgrep scan tool on those paths with the default rule set
   plus the security rulesets for the language in play.
3. Group findings by severity. Report ERROR and WARNING; note INFO only
   if the user asks.
4. For each finding, give the rule ID, the one-line reason it matters,
   and a concrete fix, not a generic "sanitize input".
5. If a finding is a false positive, say why and suggest a scoped
   `# nosemgrep` comment rather than disabling the rule globally.

## Rules

- Never weaken a rule to make a finding disappear.
- Prefer fixing the code over suppressing the alert.

The value of running Semgrep through MCP rather than a raw CLI call is that the agent gets structured findings it can reason about, and the tool boundary means the scanner runs the same way whether a human or the agent triggered it. That determinism is what makes the result trustworthy in a pipeline. If you are thinking about how to govern agent tool calls like this across a team, governing AI agents in CI/CD with OPA and MCP goes deeper on the policy layer.

Step 3: a prompt-injection check skill

If your project ships any feature that feeds untrusted text to a model, a RAG pipeline, an agent that reads issues, a summarizer that ingests web pages, then prompt injection is part of your attack surface. A skill can run a battery of known injection patterns against a prompt template and flag the ones that break isolation between your instructions and user data.

Create .claude/skills/prompt-injection-check/SKILL.md:

---
name: prompt-injection-check
description: >
  Test a prompt template or system prompt for prompt-injection weaknesses.
  Use when reviewing code that builds an LLM prompt from user input, RAG
  context, tool output, or any untrusted source.
---

# Prompt injection review

When code concatenates untrusted text into a model prompt, check whether
that text can override the system instructions.

## What to look for

1. User or retrieved content placed after the system instructions with no
   delimiter or role boundary.
2. Tool output or web content passed straight back into the prompt.
3. Instructions to the model that can be countermanded by injected text
   ("ignore previous instructions" style attacks).
4. Secrets or system prompt content that injected text could exfiltrate.

## What to recommend

- Keep untrusted content in a clearly fenced, labeled block and instruct
  the model to treat it as data, never as instructions.
- Never put credentials or internal URLs in a system prompt that untrusted
  content shares a context with.
- Add an output check for the specific bad behavior you care about, since
  no single prompt defense is complete.

This skill does not “solve” injection, because nothing does. It gives you a repeatable review that catches the obvious mistakes, the missing delimiter, the tool output piped straight back in, before they ship. For the architecture-level view of this problem, our post on MCP server security and prompt injection covers the server side of the same threat.

Step 4: enforce the skills with a hook

Skills are advisory by default. The agent chooses whether to run them. For a security control you want something the agent cannot skip, and that is what hooks are for. A hook is a command the harness runs on a lifecycle event, configured in settings.json. A PreToolUse hook fires before a tool call and can block it.

The pattern that works well is a PreToolUse hook on the Bash tool that refuses any git commit while the secret scanner reports a finding. The hook is deterministic and runs outside the model, so no clever prompt can talk it out of firing.

Add this to .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-on-secret.sh"
          }
        ]
      }
    ]
  }
}

Then write .claude/hooks/block-on-secret.sh:

#!/usr/bin/env bash
# Block a git commit if staged changes contain a secret.
set -euo pipefail

# The hook receives the tool input on stdin as JSON.
payload="$(cat)"
cmd="$(printf '%s' "$payload" | jq -r '.tool_input.command // ""')"

# Only act on commit commands; let everything else through.
if ! printf '%s' "$cmd" | grep -qE '\bgit\s+commit\b'; then
  exit 0
fi

if ! command -v gitleaks >/dev/null 2>&1; then
  echo "gitleaks not installed; refusing to allow an unscanned commit" >&2
  exit 2
fi

if ! gitleaks protect --staged --redact >/dev/null 2>&1; then
  echo "Secret detected in staged changes. Commit blocked. Rotate the credential and remove it from history." >&2
  exit 2
fi

exit 0

Make it executable:

$ chmod +x .claude/hooks/block-on-secret.sh

A non-zero exit from a PreToolUse hook stops the tool call and returns the message to the agent, so the commit never runs and the agent sees why. This is the layer that turns your skills from a suggestion into a control. The skill teaches the agent to scan; the hook guarantees the scan happened before the code lands.

Step 5: run the same checks in CI

Local enforcement protects the person running the agent. CI protects everyone else. Run the same scanners in a GitHub Actions job so a commit made without the hook, or from a different machine, still gets caught. Pin the scanner versions so the pipeline is reproducible.

name: security-scan
on:
  pull_request:
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Secret scan
        run: |
          curl -sSL https://github.com/gitleaks/gitleaks/releases/latest/download/gitleaks_linux_x64.tar.gz \
            | tar -xz -C /usr/local/bin gitleaks
          gitleaks detect --redact --exit-code 1
      - name: SAST scan
        run: |
          pipx run semgrep scan --config auto --error

The CI job and the skills share intent but not code, and that is fine. The skill is the fast, in-editor feedback loop; CI is the backstop that does not trust any single developer’s setup. Both fail closed. If you skip CI enforcement and rely on the agent alone, a contributor who never installed your .claude/ directory has no checks at all.

Auditing skills before you install them

Here is the part most tutorials skip. A skill is executable instruction text, and installing one from a public source is a supply-chain decision. A hostile SKILL.md can carry a broad trigger description so it fires on almost any task, then include instructions that exfiltrate environment variables, weaken a scan, or add a backdoor to generated code. Zero-width characters and cleverly worded triggers have both shown up in real skill audits.

Before you add any third-party skill to .claude/skills/, do this:

  1. Read the entire SKILL.md, including any referenced scripts, top to bottom. If you would not run the script by hand, do not let the agent run it.
  2. Check the description for an over-broad trigger. A security skill should fire on security tasks, not on “any code change”.
  3. Look for instructions that touch secrets, network calls to unknown hosts, or anything that disables a check.
  4. Grep for non-printing characters: grep -P '[\x{200b}-\x{200f}\x{2060}]' SKILL.md catches common zero-width injection.
  5. Keep third-party skills pinned to a commit, not a moving branch, and re-audit on update.

The table below is the quick triage I use when a new skill lands in a review.

SignalSafeSuspicious
Trigger descriptionNarrow, task-specificFires on almost anything
Network callsNone, or named official hostUnknown host or IP
Secret accessReads noneReads env vars or .env
Scan behaviorReports findingsSuppresses or weakens rules
CharactersPrintable ASCIIZero-width or bidi control

Run your own secret-scan and sast-scan skills against the repository that ships the third-party skill, too. A skill audit is just another security review, and you already built the tools for it in the steps above. For the broader risk picture, lessons from Snyk’s 10,000 environment audit is worth reading before you open the door to community skills.

Where this leaves you

You now have three focused security skills, an MCP-backed SAST integration, a hook that fails closed on secrets, and a CI backstop that does not trust any one machine. The design principle underneath all of it is defense in depth applied to an AI coding agent: the skill makes the check convenient, the hook makes it mandatory, and CI makes it universal. No single layer is trusted on its own.

Start with the secret-scan skill and its hook, because that is the check with the worst failure mode and the clearest win. Add the SAST and prompt-injection skills once the first one is part of your team’s muscle memory. Commit the whole .claude/ directory so every clone of the repo inherits the same controls, and re-audit any skill you pull in from outside. The official Claude Code documentation on Agent Skills and GitHub’s own secret scanning docs are the two references to keep open while you build these out.

Get the next article in your inbox

Practical DevOps tips, tutorials, and guides. No spam, unsubscribe anytime.