Agent Skills is a managed, hardened skill registry. Unlike open marketplaces where over 13% of skills contain critical issues, every skill and every tool in this repository is designed with security as a first-class constraint β not an afterthought.
Security here means three things: protecting your environment (CLI defense-in-depth), protecting your context window (MCP progressive disclosure), and protecting your trust (supply chain integrity).
We directly address the threats identified in the Snyk 2026 Agent Threat Report:
| Threat | Public Marketplaces | Agent Skills Guarantee |
|---|---|---|
| Malicious Payloads | Obfuscated code, binaries, or "black box" instructions | 100% Open Source: No binaries, fully readable text/code. Every line is auditable. |
| Credential Theft | Skills silently exfiltrating env vars to remote servers | Static Analysis: CI/CD pipeline blocks skills with suspicious network calls or secret access. |
| Supply Chain Attacks | Authors pushing malicious updates to existing skills | Immutable Integrity: Lockfiles and content-hashing ensure code never changes without your explicit upgrade. |
| Prompt Injection | Hidden instructions to hijack agent behavior ("jailbreaks") | Human Curation: Every prompt is manually code-reviewed by maintainers for safety boundaries. |
The CLI installer (packages/cli) implements multiple independent security layers. Each operation passes through all of them β defense-in-depth means no single bypass is enough.
Every skill name and file path is sanitized before use:
// packages/cli/src/services/installer.ts
const sanitizeName = (name: string): string => {
return (
name
.replace(/[/\\]/g, '') // Remove path separators
.replace(/[\0:*?"<>|]/g, '') // Remove null bytes and Windows-forbidden chars
.replace(/^[.\s]+|[.\s]+$/g, '') // Strip leading/trailing dots and spaces
.replace(/\.{2,}/g, '') // Collapse consecutive dots (no "..")
.replace(/^\.+/, '') // No leading dots (hidden files)
.substring(0, 255) || // Enforce filesystem name length limit
'unnamed-skill'
)
}This blocks: ../../../etc/passwd, skill\0name, /etc/passwd, skill:name, .hidden, a.repeat(300).
Even after sanitization, every resolved path is verified to be strictly inside the allowed base directory:
// packages/cli/src/services/installer.ts
const isPathSafe = (basePath: string, targetPath: string): boolean => {
const normalizedBase = normalize(resolve(basePath))
const normalizedTarget = normalize(resolve(targetPath))
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase
}Both paths are fully resolved (resolve()) before comparison β relative paths, symlinks, and .. sequences are eliminated before the check. The OS-specific separator (sep) is used to prevent tricks like /allowed/dir../escape.
This guard is applied at every write, read, and delete operation: installSkillForAgent(), getInstallPath(), getCanonicalPath(), removeSkill(), and isSkillInstalled().
Symlinks are treated as untrusted by default:
lstat()notstat()β detects symlinks without following them, preventing TOCTOU attacks- Target validation β resolves the final target and verifies it is within the allowed base directory, even for chained symlinks
- Loop detection β
ELOOPerrors (circular symlink chains) are caught and the link is forcibly removed - Windows junctions β on Windows, directory junctions are used instead of symlinks for better OS-level containment
const validateSymlinkTarget = async (linkPath: string, baseDir: string): Promise<boolean> => {
const stats = await lstat(linkPath) // Never follows the link
if (stats.isSymbolicLink()) {
const target = await readlink(linkPath)
const resolvedTarget = resolve(join(linkPath, '..'), target)
return isPathSafe(baseDir, resolvedTarget) // Target must stay inside allowed dir
}
return true
}The lockfile (.agents/.skill-lock.json) is the source of truth for installed skills. It is protected by:
Schema validation (Zod): Every lockfile read is parsed through a strict Zod schema. Invalid or malformed entries are rejected and the file gracefully migrates to a clean state β no silent corruption.
Atomic writes: The lockfile is never written in-place:
1. Backup existing file β .skill-lock.json.bak
2. Write new content β .skill-lock.json.tmp
3. Atomic rename β .skill-lock.json.tmp β .skill-lock.json
If the process is killed mid-write, the old file is intact. If the rename fails, the temp file is cleaned up.
Content hashing: Each installed skill records a SHA-256 content hash computed from all its files. This enables tamper detection: if a skill file changes on disk after installation, the hash mismatch is detectable on the next operation.
Removal authorization: Skills cannot be removed unless they appear in the lockfile. The --force flag bypasses this check and is audit-logged.
Every install, update, and remove operation is appended to an audit log at ~/.config/agent-skills/audit.log (JSON Lines format):
{"action":"install","skillName":"codenavi","agents":["cursor"],"success":1,"failed":0,"timestamp":"2026-02-25T10:00:00Z"}
{"action":"remove","skillName":"codenavi","agents":["cursor"],"success":1,"failed":0,"forced":false,"timestamp":"2026-02-25T11:00:00Z"}The log is append-only β entries are never overwritten. It is the forensic record of all agent skill operations on the machine.
Every skill in the catalog is scanned with Snyk Agent Scan (formerly mcp-scan) before publishing. The scan is incremental β only skills whose content has changed since the last run are re-scanned. The scanner requires a SNYK_TOKEN environment variable (see Security scan setup for CI and fork PR behavior).
export SNYK_TOKEN=your-token # Required; get it from https://app.snyk.io/account
npm run scan # Incremental (default β only changed skills)
npm run scan -- --force # Force full re-scan of all skillsEach skill has a SHA-256 content hash computed from all its files. Results are cached in .security-scan-cache.json (gitignored). On the next run:
Content hash unchanged β load from cache (fast, no re-scan)
Content hash changed β re-scan with snyk-agent-scan, update cache
This makes the scan fast enough to run on every PR and release without slowing CI/CD.
If the scanner flags a finding that is intentional (e.g. a first-party MCP server integration), add it to the allowlist at packages/skills-catalog/security-scan-allowlist.yaml:
version: '1.0.0'
entries:
- skill: my-skill
code: W011
reason: >
Fetches from trusted first-party API at api.example.com β expected behavior,
not a credential exfiltration vector. Reviewed by maintainers on 2026-01-01.
allowedBy: github.com/username
allowedAt: '2026-01-01'
expiresAt: '2027-01-01' # Optional but strongly recommendedRules:
- Matching is
skill + codeβ no re-scan needed, takes effect immediately expiresAtis optional but strongly recommended β forces periodic review of exceptions- Expired entries automatically re-activate the finding, ensuring exceptions don't become permanent
- The allowlist is committed and reviewed in every PR β no exceptions are invisible
The scan must pass before any release. The release pipeline (release.yml) runs npm run scan as a required step when the run has access to secrets (same-repo PRs and push to main). PRs from forks do not run the scan in the normal PR workflow (GitHub does not expose repository secrets to fork workflows). To block merge until the scan passes for all PRs (including forks), use GitHub Merge Queue and require the "Security Scan (merge queue)" status check. See Security scan setup for details. A failed scan blocks the release when the scan runs.
The @nearx/skills-mcp server (packages/mcp) has a narrower threat surface by design:
- Read-only β the server has no write access to the registry; it only reads from CDN
- No authentication β the server runs locally over stdio; there is no network-exposed endpoint
- Path validation β
fetch_skill_filesvalidates every requested file path against the registry'sfiles[]array before any network call; it is impossible to fetch an arbitrary URL - No local filesystem access β the server fetches from CDN only; it never reads or writes local files
- Stdout reserved for JSON-RPC β all logging goes exclusively to
stderrto prevent protocol corruption
This repository is a collection of curated skills intended to benefit the community. We deeply respect the intellectual property and wishes of all creators.
If you are the author of any content included here and would like it removed or updated, please open an issue or contact the maintainers directly. We will act promptly.
For licensing details, see π License and Attribution in the README.
Please do not open a public GitHub issue for security vulnerabilities.
To report a vulnerability, open a GitHub Security Advisory (private, only visible to maintainers).
Include:
- A description of the vulnerability
- Steps to reproduce
- Affected component (
cli,mcp,skills-catalog, a specific skill) - Potential impact
We aim to acknowledge reports within 48 hours and resolve confirmed vulnerabilities within 14 days. We will credit reporters in the fix commit unless anonymity is requested.