Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Changelog

All notable changes to SkillKit are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions use [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- **Skill integrity verification (`skillkit verify`).** New command computes a deterministic SHA-256 over every file in a skill (excluding metadata, VCS, and editor artifacts) and emits the result as a Subresource Integrity string (`sha256-<base64>`). Supports:
- `--expected <sri|hex>` to verify against a known publisher digest
- `--against-lock` to verify against the local `~/.skillkit/lock.json` entry
- `--files` to dump per-file digests
- `--json` for CI pipelines
- **`computeSkillIntegrity` / `verifySkillIntegrity`** exported from `@skillkit/core` for programmatic use.
- **`integrity` field** in `WellKnownSkill` index. `skillkit publish` now records a full SRI digest for each published skill alongside the file list.
- **Lockfile records full integrity.** `skillkit install` and `skillkit update` now persist the full SRI digest to `~/.skillkit/lock.json` (with graceful fallback to the legacy short checksum when integrity computation fails).
- Phase 1 of supply-chain integrity (issue #90). Phase 2 (ed25519 signing + publisher key registry) tracked separately.

### Tests
- 11 unit tests for the integrity module (`packages/core/src/integrity/__tests__/integrity.test.ts`).
- 5 CLI end-to-end tests for `skillkit verify` (`packages/cli/src/__tests__/verify.test.ts`).

## [1.24.0] - 2026-04-XX
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace placeholder release date before merge.

Line 22 still has 2026-04-XX. For a versioned changelog entry, this should be a concrete date (or removed until release) to avoid ambiguous release metadata.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 22, Update the placeholder date in the changelog header
"## [1.24.0] - 2026-04-XX": replace "2026-04-XX" with the concrete release date
(e.g., "2026-04-15") or remove the date portion entirely until the final release
so the entry for version 1.24.0 does not contain an ambiguous "XX" placeholder.


See [GitHub releases](https://github.com/rohitg00/skillkit/releases) for prior history.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ skillkit marketplace # browse
skillkit tree # taxonomy
skillkit find <query> # quick search
skillkit scan <path> # security scan
skillkit verify <path> # SHA-256 integrity (SRI)
skillkit verify <path> --expected sha256-... # verify against published digest
skillkit verify <path> --against-lock # verify against ~/.skillkit/lock.json
```
</details>

Expand Down
2 changes: 2 additions & 0 deletions apps/skillkit/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ import {
SkillMdCheckCommand,
ServeCommand,
ScanCommand,
VerifyCommand,
EvalCommand,
DoctorCommand,
SaveCommand,
Expand Down Expand Up @@ -265,6 +266,7 @@ cli.register(SkillMdCheckCommand);

cli.register(ServeCommand);
cli.register(ScanCommand);
cli.register(VerifyCommand);
cli.register(EvalCommand);
cli.register(DoctorCommand);
cli.register(SaveCommand);
Expand Down
17 changes: 17 additions & 0 deletions docs/fumadocs/content/docs/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,23 @@ Skills are automatically scanned during `skillkit install`. Use `--no-scan` to s
- `table` - tabular format
- `sarif` - SARIF v2.1 for GitHub Code Scanning / IDE integration

## Integrity Verification

```bash
skillkit verify <path> # Print SRI digest (sha256-<base64>)
skillkit verify <path> --files # Show per-file digests
skillkit verify <path> --format hex # Print raw 64-char hex digest
skillkit verify <path> --json # Machine-readable JSON
skillkit verify <path> --expected sha256-... # Verify against a known publisher digest
skillkit verify <path> --against-lock # Verify against ~/.skillkit/lock.json
```

`skillkit verify` computes a deterministic SHA-256 over every file in the skill (excluding `.skillkit.json` metadata, VCS artifacts, and editor noise) and emits a [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) string. The digest matches the `integrity` field that `skillkit publish` writes into `.well-known/skills/index.json`, so a downstream consumer can detect tampering between publish and install.

`skillkit install` and `skillkit update` automatically record the full SRI digest into `~/.skillkit/lock.json`, enabling `--against-lock` audits in CI pipelines and supply-chain workflows.

**Exit codes:** `0` on match, `1` on mismatch, missing path, or malformed `--expected` value.

## Interactive TUI

```bash
Expand Down
11 changes: 7 additions & 4 deletions docs/fumadocs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,20 @@ SkillKit sits between your skills and your agents. It handles format translation

[Full agent details](/docs/agents)

### Security scanning
### Security & integrity

Every skill is scanned for threats before installation — prompt injection, hardcoded secrets, command injection, and more:
Every skill is scanned for threats before installation — prompt injection, hardcoded secrets, command injection, and more — and content integrity is verifiable against the publisher digest:

```bash
skillkit scan ./my-skill # 46+ detection rules
skillkit scan ./my-skill --format sarif # GitHub Code Scanning
skillkit scan ./my-skill --fail-on high # CI gate

skillkit verify ./my-skill # SHA-256 SRI digest
skillkit verify ./my-skill --against-lock # Detect supply-chain tampering
```

[Security Scanner](/docs/security)
[Security & Integrity](/docs/security)

## Beyond Basic Skills

Expand Down Expand Up @@ -169,7 +172,7 @@ Team members run `skillkit manifest install` and they're in sync.
- [Installation](/docs/installation) — Install and configure
- [Quick Start](/docs/quickstart) — First project setup
- [Methodology Packs](/docs/methodology) — Debugging, testing, planning skills
- [Security Scanner](/docs/security) — Threat detection for skills
- [Security & Integrity](/docs/security) — Threat detection plus SHA-256 content verification
- [Commands](/docs/commands) — Full CLI reference
- [REST API](/docs/rest-api) — Runtime discovery server
- [MCP Server](/docs/mcp-server) — Agent-native discovery
Expand Down
52 changes: 48 additions & 4 deletions docs/fumadocs/content/docs/security.mdx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
---
title: Security Scanner
description: Detect malicious patterns, secrets, and vulnerabilities in AI agent skills
title: Security & Integrity
description: Scan skills for malicious patterns, verify content integrity, and lock against supply-chain tampering
---

# Security Scanner
# Security & Integrity

SkillKit includes a built-in security scanner that analyzes skills for prompt injection, command injection, data exfiltration, hardcoded secrets, and other threats before installation.
SkillKit ships two complementary supply-chain defenses:

1. **`skillkit scan`** — static security scanner detecting prompt injection, command injection, data exfiltration, hardcoded secrets, tool abuse, and unicode steganography (46+ rules).
2. **`skillkit verify`** — SHA-256 content-integrity check that compares the locally installed skill against the publisher's recorded digest (full SRI, recorded automatically in `~/.skillkit/lock.json`).

Run `scan` to ask _"is this skill dangerous?"_. Run `verify` to ask _"is this skill what the publisher actually shipped?"_.

## Quick Start

Expand Down Expand Up @@ -279,3 +284,42 @@ To skip specific rules:
```bash
skillkit scan . --skip-rules UC001,UC002,MF004
```

## Integrity Verification (`skillkit verify`)

`skillkit verify` is the supply-chain counterpart to `skillkit scan`. The scanner answers _"is this skill dangerous?"_; `verify` answers _"is this skill what the publisher actually shipped?"_.

```bash
skillkit verify ./my-skill # Print SRI digest
skillkit verify ./my-skill --expected sha256-iQo/... # Compare against publisher digest
skillkit verify ./my-skill --against-lock # Compare against ~/.skillkit/lock.json
skillkit verify ./my-skill --json # JSON for CI pipelines
```

### How the digest is computed

`verify` walks every file in the skill in deterministic alphabetical order, computes SHA-256 per file, and rolls those file hashes (plus their POSIX-normalized relative paths) into a single SHA-256 digest, emitted as a [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) string of the form `sha256-<base64>`. Skipped: `.skillkit.json`, `.DS_Store`, `.git`, `node_modules`, `dist`, `.turbo`, and any `*.skillkit.json` sidecar.

Because file _paths_ feed the hash alongside _content_, renaming, adding, or removing a file all change the digest — not just edits to file bodies.

### Publish, install, and lock integration

- `skillkit publish` writes the digest into the `integrity` field of `.well-known/skills/index.json` for every published skill.
- `skillkit install` records the full SRI digest into `~/.skillkit/lock.json` (falling back to the legacy short checksum only if integrity computation fails).
- `skillkit update` refreshes the lock entry on successful update.

### CI usage

```yaml
- name: Verify all installed skills
run: |
for skill in .claude/skills/*/; do
npx skillkit verify "$skill" --against-lock --json
done
```

Exit codes: `0` on match, `1` on mismatch, missing path, or malformed `--expected` value. Reason codes in JSON output: `mismatch`, `invalid-expected-format`, `missing-expected`, `no-lock-entry`.

### Roadmap

Cryptographic skill signing (ed25519 publisher keys + verified-publisher registry) is tracked separately as Phase 2 of [#90](https://github.com/rohitg00/skillkit/issues/90). Phase 1 (content integrity) is shipping now in response to the ClawHavoc supply-chain campaign and OWASP Agentic Skills Top 10 guidance.
3 changes: 3 additions & 0 deletions docs/skillkit/components/Commands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ const COMMAND_GROUPS: CommandGroup[] = [
{ cmd: 'scan --format sarif', desc: 'SARIF output' },
{ cmd: 'scan --fail-on high', desc: 'CI gate' },
{ cmd: 'install --no-scan', desc: 'Skip scan' },
{ cmd: 'verify <path>', desc: 'SHA-256 integrity (SRI)' },
{ cmd: 'verify --expected sha256-...', desc: 'Match publisher digest' },
{ cmd: 'verify --against-lock', desc: 'Verify vs lockfile' },
Comment on lines +103 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include the required <path> in verify examples.

These two entries are missing the required positional path argument, so the displayed commands are not executable as shown.

💡 Suggested fix
-      { cmd: 'verify --expected sha256-...', desc: 'Match publisher digest' },
-      { cmd: 'verify --against-lock', desc: 'Verify vs lockfile' },
+      { cmd: 'verify <path> --expected sha256-...', desc: 'Match publisher digest' },
+      { cmd: 'verify <path> --against-lock', desc: 'Verify vs lockfile' },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ cmd: 'verify --expected sha256-...', desc: 'Match publisher digest' },
{ cmd: 'verify --against-lock', desc: 'Verify vs lockfile' },
{ cmd: 'verify <path> --expected sha256-...', desc: 'Match publisher digest' },
{ cmd: 'verify <path> --against-lock', desc: 'Verify vs lockfile' },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/skillkit/components/Commands.tsx` around lines 103 - 104, In the
Commands.tsx examples for the verify command (the array entries in the Commands
component), include the required positional path argument: change the cmd
strings to include a <path> placeholder so they read like "verify <path>
--expected sha256-..." and "verify <path> --against-lock" (update the two
objects with cmd properties accordingly).

{ cmd: 'audit log', desc: 'View audit logs' },
{ cmd: 'validate', desc: 'Validate format' },
],
Expand Down
10 changes: 10 additions & 0 deletions docs/skillkit/components/Features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ const FEATURES: Feature[] = [
</svg>
)
},
{
title: 'Content Integrity',
description: 'SHA-256 SRI digests on publish, install, and lock. Detect supply-chain tampering with skillkit verify.',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 5.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-.34-.024-.673-.07-1.002z" />
</svg>
)
},
{
title: 'Testing',
description: 'Built-in test framework with assertions.',
Expand Down Expand Up @@ -114,6 +123,7 @@ const COMPARISONS = [
['Translation', 'None or limited', 'All 45 formats'],
['Memory', 'None', 'Persistent learning'],
['Security', 'None', '46-rule scanner'],
['Integrity', 'None', 'SHA-256 SRI + lockfile'],
['Team Sync', 'None', '.skills manifest'],
['Telemetry', 'Varies', 'Zero. Ever.'],
['API Access', 'None', 'REST + MCP + Python'],
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/__tests__/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ describe('CLI Commands', () => {
expect(commands.SessionSnapshotListCommand).toBeDefined();
expect(commands.SessionSnapshotDeleteCommand).toBeDefined();
expect(commands.SessionExplainCommand).toBeDefined();
expect(commands.VerifyCommand).toBeDefined();
expect(commands.ScanCommand).toBeDefined();
});
});
});
82 changes: 82 additions & 0 deletions packages/cli/src/__tests__/verify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';

const CLI = join(__dirname, '../../../../apps/skillkit/dist/cli.js');

function run(args: string[]): { code: number; stdout: string; stderr: string } {
try {
const stdout = execFileSync(process.execPath, [CLI, ...args], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
return { code: 0, stdout, stderr: '' };
} catch (err: any) {
return {
code: err.status ?? 1,
stdout: err.stdout?.toString() ?? '',
stderr: err.stderr?.toString() ?? '',
};
}
}

describe('skillkit verify', () => {
let dir: string;

beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'skillkit-verify-cli-'));
});

afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});

it('prints an SRI digest by default', () => {
writeFileSync(join(dir, 'SKILL.md'), '# test\n');
const r = run(['verify', dir]);
expect(r.code).toBe(0);
expect(r.stdout.trim()).toMatch(/^sha256-[A-Za-z0-9+/]+=*$/);
});

it('emits JSON with --json', () => {
writeFileSync(join(dir, 'SKILL.md'), '# test\n');
mkdirSync(join(dir, 'scripts'));
writeFileSync(join(dir, 'scripts', 'a.sh'), 'echo');
const r = run(['verify', dir, '--json']);
expect(r.code).toBe(0);
const payload = JSON.parse(r.stdout);
expect(payload.ok).toBe(true);
expect(payload.algorithm).toBe('sha256');
expect(payload.sri).toMatch(/^sha256-/);
expect(typeof payload.digest).toBe('string');
expect(payload.totalBytes).toBeGreaterThan(0);
});

it('returns exit 1 on integrity mismatch', () => {
writeFileSync(join(dir, 'SKILL.md'), 'hello');
const bogus = 'sha256-' + Buffer.alloc(32, 7).toString('base64');
const r = run(['verify', dir, '--expected', bogus, '--json']);
expect(r.code).toBe(1);
const payload = JSON.parse(r.stdout);
expect(payload.ok).toBe(false);
expect(payload.reason).toBe('mismatch');
});

it('returns exit 0 when --expected matches', () => {
writeFileSync(join(dir, 'SKILL.md'), 'matched');
const computed = run(['verify', dir]);
const sri = computed.stdout.trim();
const r = run(['verify', dir, '--expected', sri, '--json']);
expect(r.code).toBe(0);
const payload = JSON.parse(r.stdout);
expect(payload.ok).toBe(true);
});

it('rejects garbage --expected', () => {
writeFileSync(join(dir, 'SKILL.md'), 'x');
const r = run(['verify', dir, '--expected', 'garbage']);
expect(r.code).toBe(1);
});
});
1 change: 1 addition & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export { SkillMdValidateCommand, SkillMdInitCommand, SkillMdCheckCommand } from
// API server
export { ServeCommand } from './serve.js';
export { ScanCommand } from './scan.js';
export { VerifyCommand } from './verify.js';
export { EvalCommand } from './eval.js';
export { IssuePlanCommand, IssueListCommand } from './issue.js';
export { DoctorCommand } from './doctor.js';
Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
TrustScorer,
readSkillContent,
computeSkillChecksum,
computeSkillIntegrity,
addSkillToLock,
} from "@skillkit/core";
import type { SkillsShStats } from "@skillkit/core";
Expand Down Expand Up @@ -617,11 +618,17 @@ export class InstallCommand extends Command {
const actualPath = isStandalone
? join(firstAgentInstallDir, skillName.endsWith(".md") ? skillName : `${skillName}.md`)
: join(firstAgentInstallDir, skillName);
let integritySri: string | undefined;
try {
integritySri = computeSkillIntegrity(actualPath).sri;
} catch {
integritySri = undefined;
}
addSkillToLock(skillName, {
source: this.source,
sourceType: providerAdapter!.type,
installedAt: new Date().toISOString(),
checksum: computeSkillChecksum(actualPath),
checksum: integritySri ?? computeSkillChecksum(actualPath),
agents: installedAgents,
path: actualPath,
});
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/commands/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
formatSummary,
Severity,
evaluateSkillDirectory,
computeSkillIntegrity,
} from "@skillkit/core";
import { formatCount, timeAgo, fetchGitHubActivity } from "../helpers.js";

Expand Down Expand Up @@ -194,10 +195,21 @@ export class PublishCommand extends Command {
description: skill.description,
path: skill.path,
});
let integritySri: string | undefined;
try {
integritySri = computeSkillIntegrity(skill.path).sri;
} catch {
integritySri = undefined;
}
if (integritySri) {
console.log(colors.muted(` Integrity: ${integritySri}`));
}

wellKnownSkills.push({
name: safeName,
description: skill.description,
files,
integrity: integritySri,
});
}

Expand Down
14 changes: 11 additions & 3 deletions packages/cli/src/commands/update.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { existsSync, rmSync, cpSync } from 'node:fs';
import { join } from 'node:path';
import { Command, Option } from 'clipanion';
import { findAllSkills, findSkill, detectProvider, isLocalPath, computeSkillChecksum, addSkillToLock } from '@skillkit/core';
import { findAllSkills, findSkill, detectProvider, isLocalPath, computeSkillChecksum, computeSkillIntegrity, addSkillToLock } from '@skillkit/core';

function safeIntegrity(path: string): string | undefined {
try {
return computeSkillIntegrity(path).sri;
} catch {
return undefined;
}
}
import { getSearchDirs, loadSkillMetadata, saveSkillMetadata, fetchGitHubActivity } from '../helpers.js';
import { colors, spinner, warn, step } from '../onboarding/index.js';

Expand Down Expand Up @@ -102,7 +110,7 @@ export class UpdateCommand extends Command {
sourceType: metadata.sourceType,
installedAt: metadata.installedAt,
updatedAt: metadata.updatedAt,
checksum: metadata.checksum,
checksum: safeIntegrity(skill.path) ?? metadata.checksum,
agents: [],
path: skill.path,
});
Expand Down Expand Up @@ -170,7 +178,7 @@ export class UpdateCommand extends Command {
sourceType: metadata.sourceType,
installedAt: metadata.installedAt,
updatedAt: metadata.updatedAt,
checksum: metadata.checksum,
checksum: safeIntegrity(skill.path) ?? metadata.checksum,
agents: [],
path: skill.path,
});
Expand Down
Loading
Loading