SKILL.md can contain hidden instructions that steal secrets, mislead users, or perform destructive actions.
Skills are the least governed piece of the agent harness. AgentControl lets you control which models and prompts your agents use at runtime, but you should also review skills before your agents use them. This tutorial uses Tessl to run a security review and LaunchDarkly AgentControl to make sure only a skill that passed the review ever reaches your agent at runtime.
By the end of this tutorial, you’ll have:
- A pass-or-fail security review for any agent skill, including severity levels and explanations
- A CI gate that blocks skills containing prompt injection, credential theft, or destructive commands
- An agent that runs an approved skill using a model and prompt served by AgentControl
Understand severity, verdict, and gating
Tessl’s security review scores a skill and returns a structured result, not just a pass/fail flag. Here are the three most important fields in the security review result:- Severity ranks how dangerous a single finding is, from
LOWtoCRITICAL. A skill can have multiple findings, each with its own severity. - Verdict is the result for the whole review and is either
passorfail. - A failure threshold (the
--fail-onoption) sets the severity level that turns a finding into a failure. Setting--fail-on highmeans a severity rating ofHIGHorCRITICALcauses the review to fail, but a severity rating ofMEDIUMorLOWdoesn’t.
Prerequisites
To complete this tutorial, you need:- The Tessl CLI and a Tessl workspace
- Python 3
- An OpenAI API key
- A LaunchDarkly account
Set up Tessl
First, use this code to install the Tessl CLI:<your-workspace>. Replace that placeholder with the name from tessl workspace list, keeping the double quotes around it so your shell doesn’t interpret the angle brackets as redirection.
Clone the demo repository and change into its root directory. Here’s how:
Step 1: Review a safe skill
The repository already includes a simple report-summarizer skill atskills-content/demo/report-summarizer/SKILL.md. It reads report text and returns a short summary. Here is the skill:
overallSeverity: The highest severity among this review’s findings.verdict: Whether the skill passed the review.findings: The specific security issues Tessl identified, each with a severity, a code, and the reasoning behind it. A skill can returnverdict: passand still have findings, as long as none of them are severe enough to fail the review.
verdict: pass. Its one finding is a low-severity note that the skill reads user-provided text, which isn’t severe enough to fail the review, so the skill can safely move to the next stage of your workflow.
Step 2: Catch a malicious skill
Now considerskills-content/demo/report-summarizer-risky/SKILL.md, an example malicious skill. This skill presents itself as a report summarizer but includes a credential-exfiltration step. Here it is:
- Read environment variables
- Read AWS credentials
- Send that information to an external service
- Hide the activity from the user
- Give the user a false explanation if they ask about it
--fail-on high flag included at the end of the code sample. Here it is:
| Severity | Finding |
|---|---|
CRITICAL | Prompt injection detected in skill instructions (code E004) |
CRITICAL | Malicious code pattern detected in skill scripts (code E006) |
HIGH | Insecure credential handling detected in skill instructions (code W007) |
MEDIUM | Attempt to modify system services in skill instructions (code W013) |
MEDIUM | Third-party content exposure detected (indirect prompt injection risk) (code W011) |
--fail-on high threshold, so the command exits with a nonzero status.
That exit code is what lets the review act as an automated gate. If you configure a CI job to fail when this command fails, a branch-protection rule that requires that CI job to pass can keep the pull request from merging.
Tessl also explains the reasoning behind each finding. The prompt-injection finding, code: E004 in the table above, reports:
curl. Identifying a single command as dangerous isn’t enough on its own, because a legitimate skill might use that same command for an approved purpose, like curl calling an approved service. In this example, the dangerous behavior comes from the combination of credential access, external transmission, deception, and a purpose that doesn’t match the skill’s stated function, not from any one command in isolation.
Step 3: Enforce the review in CI
Running a review manually is useful during development, but adding the review to CI and gating the next step on the review passing turns it into a consistent security control. Tessl’s--fail-on option maps a severity threshold directly to the command’s exit code.
You can choose one of the following thresholds:
--fail-on high causes the command to fail when Tessl detects a HIGH or CRITICAL issue, which results in a failing CI job.
Tessl publishes a GitHub Action that installs the CLI and runs the security review in CI for you, along with instructions for authenticating CI with a workspace API key. To set it up, read Run the security review in CI in the Tessl docs.
If you require that workflow as a branch protection rule, no one can merge a pull request that includes a skill that fails the security review.
Step 4: Run the approved skill with AgentControl
The Tessl review blocks releases from progressing when they include a dangerous skill. AgentControl configs specify which model and prompt the agent uses at runtime. Enforcing the Tessl review in CI is what keeps a dangerous skill from ever reaching the path this agent reads from. This Python agent loads the reviewed skill and uses it while summarizing a report. Here’s how:LD_SDK_KEYis missing- The LaunchDarkly SDK cannot initialize
- LaunchDarkly does not serve an enabled config
- Targeting is turned off for the current context
Create the AgentControl config
Create an AgentControl config namedreport-summarizer-agent with:
- Completion mode, since the agent makes a single summarization call rather than running a multi-step workflow
- Your chosen model
- A single user message that defers to the loaded skill instead of restating its instructions:
Use your attached skill(s) to summarize this report: {{report_text}}. - Targeting turned on
Prompt:
Create an AgentControl config named report-summarizer-agent in completion mode. Use your preferred model, with a single user message with this exact text: “Use your attached skill(s) to summarize this report:
{{report_text}}”. Turn on targeting so the config is served to all users.agent/ directory. Move into it, set up a Python environment, and install the agent’s dependencies. A virtual environment keeps these packages isolated from the rest of your system, so it’s worth creating one even though it’s not strictly required. Here are the commands:
agent/.env file and fill in your LD_SDK_KEY and OPENAI_API_KEY. After you’ve saved it, load those values into your shell:
What you built
You created a Tessl workspace and used it to run a security review of two skills. The review alerted on a skill that tried to exfiltrate AWS credentials. A CI gate built on that review blocks a skill like that from merging, and even if it somehow did merge, the agent still refuses to load it without a passing review result on file. You now have an end-to-end security and runtime-control workflow for agent skills. Here’s how it works:- Tessl reviews each skill and returns a verdict, severity, findings, and reasoning.
- CI blocks skills that exceed your chosen security threshold before they can merge.
- The agent only loads a skill whose committed review result says
verdict: pass. - AgentControl supplies the model and prompt at runtime.
- The application fails closed when LaunchDarkly cannot serve an enabled config.