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

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 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, 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 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), 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

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 cannot be modified at runtime.

Building Validated and Sanitized Prompts

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

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 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:

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 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 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 or other files cannot inject malicious instructions into your production environment.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →