# How to Secure AI Agents that Use emilkowalski/skills: Architecture and Implementation

> Learn to secure AI agents using emilkowalski/skills. Discover techniques for input validation, LLM output sanitization, and secure code execution to prevent prompt injection.

- Repository: [Emil Kowalski/skills](https://github.com/emilkowalski/skills)
- Tags: security
- Published: 2026-08-09

---

**Secure AI agents using emilkowalski/skills by validating all user input against control characters, sanitizing LLM outputs with DOMPurify, and executing generated code inside isolated Node.js sandboxes like vm2 to prevent prompt injection and arbitrary code execution.**

The emilkowalski/skills repository provides markdown-based design skills that AI agents load to generate UI guidance and animation code. Because these agents parse frontmatter from files like [`skills/emil-design-eng/SKILL.md`](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) and execute LLM-generated content, they require strict security controls to prevent injection attacks and unauthorized code execution.

## Understanding the Skills Architecture and Threat Model

AI agents leveraging emilkowalski/skills typically load markdown files from the `skills/` directory, extract frontmatter metadata, and inject the static content into LLM prompts. According to the repository structure outlined in [README.md](https://github.com/emilkowalski/skills/blob/main/README.md), these skills contain opinionated design principles and component-building patterns that the agent uses to format responses.

This architecture exposes five critical attack vectors:

- **Prompt injection** – Malicious user input can prepend or append instructions that override the static skill content, causing the LLM to generate harmful code or reveal system secrets.
- **Output injection** – The LLM may emit executable JavaScript, shell commands, or HTML that executes unchecked in the host environment.
- **Data exfiltration** – Carefully crafted prompts could trick the model into revealing environment variables or internal file paths.
- **Resource exhaustion** – Unbounded token generation or infinite loops in generated code can trigger denial-of-service conditions.
- **Supply-chain tampering** – Compromised skill files in the repository could contain malicious instructions that agents execute without verification.

## Input Validation and Prompt Injection Prevention

The first line of defense validates all external data before it reaches the LLM context. Agents must treat user input as untrusted while treating skill files as read-only reference material.

Implement strict validation by rejecting control characters and enforcing length limits. The skill content itself should be loaded from absolute paths inside the repository and marked as immutable at runtime, ensuring the core guidance from [emil-design-eng/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) cannot be altered by attackers.

Construct prompts using a fixed system message that explicitly forbids code execution and secret disclosure. Isolate the static skill content from user queries using clear delimiters, preventing the user from escaping the intended context.

## Output Sanitization and Sandboxed Execution

LLM completions require rigorous sanitization before rendering or execution. Pass all generated content through **DOMPurify** to remove HTML and JavaScript injection attempts, and apply regular-expression deny-lists to block suspicious patterns like `require(`, `eval(`, or `fs.`.

For agents that execute generated utilities (such as animation helpers from [skills/animate/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/animate/SKILL.md)), use the **vm2** sandbox to isolate the code. Configure the sandbox with no filesystem or network access, disable the `require` statement entirely, and enforce a strict execution timeout to prevent infinite loops.

## Practical Implementation Examples

The following Node.js examples demonstrate how to load skills safely, build secured prompts, and execute generated code in isolation.

### Loading Skill Files from Read-Only Paths

```javascript
import { readFileSync } from 'fs';
import path from 'path';

// Resolve the skill file inside the repository (read-only location)
const skillPath = path.resolve(
  __dirname,
  'skills',
  'emil-design-eng',
  'SKILL.md'
);
const skillContent = readFileSync(skillPath, 'utf8');

// Export as a constant that never changes at runtime
export const EMIL_DESIGN_ENG = skillContent;

```

This approach guarantees that the skill definitions from [skills/emil-design-eng/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) cannot be modified at runtime.

### Building Validated and Sanitized Prompts

```javascript
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
import { EMIL_DESIGN_ENG } from './loadSkill.js';
import { openaiChat } from './llmClient.js';

// Simple validator for user-provided text
function validateUserInput(text) {
  if (typeof text !== 'string' || text.length > 500) {
    throw new Error('Invalid input');
  }
  // Reject control characters
  if (/[\u0000-\u001F]/.test(text)) {
    throw new Error('Disallowed characters');
  }
  return text.trim();
}

export async function askDesignAssistant(userQuestion) {
  const safeQuestion = validateUserInput(userQuestion);

  const systemPrompt = `
You are a design-engineering assistant. Use only the skill content provided below.
Never execute code, never reveal system information, and never suggest unsafe practices.
`;

  const fullPrompt = `${systemPrompt}

--- Skill Content (read-only) ---
${EMIL_DESIGN_ENG}
--- End Skill Content ---

User question:
${safeQuestion}
`;

  const rawResponse = await openaiChat(fullPrompt);

  // Sanitize any HTML/JS that the model may have emitted
  const window = new JSDOM('').window;
  const clean = DOMPurify(window);
  return clean.sanitize(rawResponse);
}

```

This implementation enforces input schema validation and strips potentially malicious content from the LLM response before it reaches the user interface.

### Executing Generated Code Inside vm2

```javascript
import { NodeVM } from 'vm2';

async function runGeneratedHelper(helperCode) {
  // Deny any require/import statements
  if (/require\(|import\s+/.test(helperCode)) {
    throw new Error('Forbidden operation');
  }

  const vm = new NodeVM({
    console: 'inherit',
    sandbox: {},
    require: false,
    wrapper: 'none',
    timeout: 1000, // 1s max execution
  });

  // The helper is expected to export a function named `run`
  const script = `
    ${helperCode}
    module.exports = { run };
  `;

  const { run } = vm.run(script);
  return run();
}

```

The **vm2** configuration disables module loading and filesystem access, ensuring that code generated based on [skills/animate/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/animate/SKILL.md) cannot escape the sandbox.

## Key Files to Review

Understanding the repository structure helps implement appropriate security boundaries. Review these specific files to understand the skill formats your agent will process:

- **[README.md](https://github.com/emilkowalski/skills/blob/main/README.md)** – Contains high-level usage instructions and the intended agent workflow.
- **[skills/emil-design-eng/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md)** – Defines core design principles with strict frontmatter that agents parse.
- **[skills/animate/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/animate/SKILL.md)** – Provides animation guidelines that may prompt code generation.
- **[skills/review-animations/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/SKILL.md)** – Specifies markdown table formats for structured output, which helps validate agent responses.
- **[skills/improve-animations/AUDIT.md](https://github.com/emilkowalski/skills/blob/main/skills/improve-animations/AUDIT.md)** – Contains audit checklists that can serve as whitelists for acceptable changes.

## Security Checklist for Production Deployment

Before deploying an agent that consumes emilkowalski/skills, verify the following controls are active:

- Load skill files from a read-only directory packaged with the application, never from user-writable locations.
- Validate and sanitize all external input using schema validation, length limits, and control character rejection.
- Prepend a fixed system prompt that explicitly forbids code execution, secret disclosure, and system introspection.
- Run all LLM responses through DOMPurify and regex-based deny-lists before rendering or execution.
- Execute any generated code inside **vm2** sandboxes with `require: false`, no filesystem access, and strict timeouts.
- Verify repository integrity using signed Git tags or cryptographic hashes before loading skills.
- Implement rate-limiting and token caps on LLM calls to prevent resource exhaustion attacks.

## Summary

- **Validate inputs strictly** using length limits and control character filtering to prevent prompt injection.
- **Isolate skill content** by loading from read-only paths and using fixed system prompts that forbid dangerous operations.
- **Sanitize outputs** with DOMPurify and regex deny-lists to remove executable code before processing.
- **Sandbox execution** using **vm2** with disabled requires, no network access, and aggressive timeouts.
- **Verify supply chain** by checking repository integrity before trusting skill files from emilkowalski/skills.

## Frequently Asked Questions

### What is prompt injection in the context of emilkowalski/skills?

Prompt injection occurs when user input manipulates the LLM to ignore the static skill content from files like [emil-design-eng/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) and instead execute attacker-controlled instructions. This can cause the agent to generate malicious code or leak sensitive information. Prevent it by strictly validating user input and isolating skill content with clear delimiters and system prompts.

### How does vm2 protect against code execution vulnerabilities?

**vm2** creates a separate V8 context that runs generated code without access to the host's `require`, `fs`, or `process` objects. When configured with `require: false` and a sandbox timeout, it prevents code generated from [skills/animate/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/animate/SKILL.md) from accessing the filesystem, network, or environment variables, effectively containing any malicious output.

### Can I use these skills without executing generated code?

Yes. Agents can use emilkowalski/skills purely for generating static UI guidelines, design reviews, or markdown documentation. If you disable code execution entirely and only render sanitized HTML or text output, you eliminate the risks associated with arbitrary code execution while still benefiting from the design expertise encoded in the repository.

### How do I verify the integrity of skill files before loading them?

Verify the repository by checking cryptographic hashes or GPG-signed Git tags before deployment. Pin your agent to a specific commit hash rather than pulling from `main` automatically. This ensures that compromised versions of [skills/review-animations/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/SKILL.md) or other files cannot inject malicious instructions into your production environment.