Skip to content

feat(integrity): SHA-256 skill integrity verification (#90 phase 1)#122

Open
rohitg00 wants to merge 1 commit into
mainfrom
feat/skill-integrity
Open

feat(integrity): SHA-256 skill integrity verification (#90 phase 1)#122
rohitg00 wants to merge 1 commit into
mainfrom
feat/skill-integrity

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented May 11, 2026

Summary

Ships phase 1 of #90 — supply-chain content integrity for SkillKit. Direct response to the Q1 2026 ClawHavoc campaign (341 malicious skills flooding ClawHub in 3 days) and OWASP Agentic Skills Top 10 guidance.

The scanner asks "is this skill dangerous?". The new verify command asks "is this skill what the publisher actually shipped?".

What ships

  • New @skillkit/core integrity module — deterministic recursive SHA-256 over every file in a skill (paths + content), emitted as a Subresource Integrity string sha256-<base64>. Skips .skillkit.json, VCS, .DS_Store, node_modules, dist, .turbo. 50 MB safety cap.
  • New skillkit verify <path> command with --expected, --against-lock, --files, --format sri|hex, --json. Exit codes 0/1, structured reason codes (mismatch, invalid-expected-format, missing-expected, no-lock-entry) for CI pipelines.
  • skillkit publish writes an integrity field per skill into .well-known/skills/index.json. Downstream consumers can now detect tampering between publish and install.
  • skillkit install and skillkit update record the full SRI digest into ~/.skillkit/lock.json (with graceful fallback to legacy short checksum if integrity computation fails).
  • Docs aligned:
    • docs/fumadocs/content/docs/security.mdx retitled "Security & Integrity" with a full verify section (digest algorithm, publish/install/lock integration, CI example, phase-2 roadmap pointer)
    • docs/fumadocs/content/docs/commands.mdx + index.mdx updated
    • docs/skillkit/components/Commands.tsx adds verify entries to the Security category
    • docs/skillkit/components/Features.tsx adds a "Content Integrity" feature card + a new comparison-matrix row
  • Root CHANGELOG.md added.

Why now

Threat Reference
ClawHavoc supply-chain campaign 341 malicious skills flooding ClawHub, Q1 2026
Snyk skill marketplace audit 13.4% of marketplace skills contain critical security issues
OWASP Agentic Skills Top 10 Recommends ed25519 + content_hash (publisher signing)
CVE-2025-59536 / CVE-2026-21852 Claude Code config injection via repository files

Phase 2 (ed25519 publisher key registry + signed publishes) is tracked in #90 and follows once this lands.

Test plan

  • 11 unit tests in packages/core/src/integrity/__tests__/integrity.test.ts (stable digest, drift detection, file-add detection, SRI/hex parsing, max-bytes guard)
  • 5 CLI E2E tests in packages/cli/src/__tests__/verify.test.ts (SRI emission, --json, match, mismatch, garbage --expected)
  • All 25 task suites green (pnpm test)
  • pnpm typecheck clean
  • pnpm build green
  • Manual smoke test on a sample skill: verify, verify --files, verify --json, verify --expected ... (match + mismatch), publish --dry-run confirms integrity field in index.json, translate + scan still work

Backwards compatibility

  • LockFile schema unchanged — checksum field type still string. Existing lockfile entries with the legacy 16-char short hash still load; skillkit verify --against-lock will report invalid-expected-format for them, and skillkit update upgrades them to SRI on next refresh.
  • Existing computeSkillChecksum (short SKILL.md-only hash) is untouched and still drives skillkit update's fast change-detection compare. The new computeSkillIntegrity is additive.
  • WellKnownSkill interface gains an optional integrity?: string field — existing consumers that ignore it continue to work.

Closes phase 1 of #90.

Summary by CodeRabbit

  • New Features

    • Added skillkit verify command to compute and verify skill integrity using SHA-256 hashing
    • Automatic integrity tracking integrated into publish, install, and update workflows
    • Support for verifying against expected digests or recorded checksums in lockfile
  • Documentation

    • Updated command reference and guides with integrity verification details
    • Enhanced security documentation to cover both scanning and integrity verification workflows
  • Tests

    • Added comprehensive test coverage for verify command and integrity computation functions

Review Change Stack

Ships supply-chain content integrity for the SkillKit ecosystem
(phase 1 of issue #90):

- New @skillkit/core integrity module computes a deterministic SHA-256
  digest over every file in a skill (excluding .skillkit.json metadata,
  VCS artifacts, editor noise) and emits a Subresource Integrity string
  (sha256-<base64>). 50 MB safety cap.
- New `skillkit verify <path>` CLI with --expected, --against-lock,
  --files, --format sri|hex, --json.
- `skillkit publish` now writes an `integrity` field per skill into
  .well-known/skills/index.json.
- `skillkit install` and `skillkit update` record the full SRI digest
  into ~/.skillkit/lock.json (graceful fallback to legacy short
  checksum if integrity computation fails).
- Docs: fumadocs security.mdx retitled "Security & Integrity" with a
  full verify section; commands.mdx + index.mdx updated; website
  Commands.tsx + Features.tsx surface the new feature and comparison
  matrix row.
- Root CHANGELOG.md added.

Phase 1 (content hashing) is the response to the ClawHavoc Q1 2026
supply-chain campaign and OWASP Agentic Skills Top 10 guidance.
Phase 2 (ed25519 signing + publisher key registry) is tracked
separately.

Tests: 11 unit tests for the integrity module, 5 CLI E2E tests for
`skillkit verify`. All 25 task suites green; typecheck clean.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
skillkit Ready Ready Preview, Comment May 11, 2026 10:06am
skillkit-docs Ready Ready Preview, Comment May 11, 2026 10:06am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

This PR introduces deterministic SHA-256 Subresource Integrity (SRI) verification for skills, including a new skillkit verify command, core integrity computation functions exported from @skillkit/core, integration into publish/install/update workflows, comprehensive test coverage, and documentation updates.

Changes

Supply-Chain Integrity Verification

Layer / File(s) Summary
Type Definitions & Exports
packages/core/src/integrity/integrity.ts, packages/core/src/integrity/index.ts, packages/core/src/providers/wellknown.ts, packages/core/src/index.ts
IntegrityOptions, IntegrityResult, VerifyResult types added; WellKnownSkill now includes optional integrity field; functions re-exported from core package.
Core Integrity Algorithms
packages/core/src/integrity/integrity.ts
computeSkillIntegrity() walks directory tree (excluding metadata), hashes files deterministically in alphabetical order, builds rollup SHA-256 digest incorporating each file's path + digest; verifySkillIntegrity() compares computed vs. expected digests with reason codes; formatIntegrity() outputs SRI or hex format; parseIntegrity() parses SRI or hex strings.
Integrity Module Tests
packages/core/src/integrity/__tests__/integrity.test.ts
Vitest suite validating stable digest computation, recursive hashing, file exclusions, content drift detection, path sensitivity, format parsing, and maxBytes enforcement.
Verify CLI Command
packages/cli/src/commands/verify.ts
New VerifyCommand resolves target paths, validates --format (sri/hex), computes integrity, verifies against --expected or --against-lock lockfile entries; supports --json and --files flags; returns exit code 0 on success, 1 on failure.
CLI Command Registration
packages/cli/src/commands/index.ts, apps/skillkit/src/cli.ts
Export and register VerifyCommand in CLI.
Well-Known Index Schema
packages/core/src/providers/wellknown.ts
generateWellKnownIndex() accepts optional integrity per skill and emits into WellKnownSkill entries.
Publish Integration
packages/cli/src/commands/publish.ts
Compute and log per-skill integrity SRI; include in well-known index entries; gracefully handle computation failures.
Install Integration
packages/cli/src/commands/install.ts
Record full integrity SRI to lockfile with fallback to legacy checksum.
Update Integration
packages/cli/src/commands/update.ts
Record integrity SRI to lockfile for local-path and remote/provider updates with fallback to metadata checksum.
Verify Command E2E Tests
packages/cli/src/__tests__/verify.test.ts
Test built CLI via execFileSync: default digest, --json output, --expected validation (match/mismatch), invalid input handling.
CLI Exports Test
packages/cli/src/__tests__/commands.test.ts
Assert VerifyCommand and ScanCommand in imports.
Changelog & Release Notes
CHANGELOG.md
Document new [Unreleased] features (verify command, core exports, well-known/lockfile persistence, tests) and placeholder [1.24.0] version.
README & Documentation Index
README.md, docs/fumadocs/content/docs/index.mdx
Add skillkit verify command reference; rename "Security scanning" to "Security & integrity"; update navigation links.
Security & Integrity Documentation
docs/fumadocs/content/docs/commands.mdx, docs/fumadocs/content/docs/security.mdx
Comprehensive docs covering verify command flags, deterministic digest computation, publish/install/update/lock integration, CI examples, exit codes, JSON schema, and Phase 2 roadmap (cryptographic signing).
Website UI Components
docs/skillkit/components/Commands.tsx, docs/skillkit/components/Features.tsx
Add three verify command variants to Commands group; add "Content Integrity" feature card; add "Integrity" row to comparison table.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • rohitg00/skillkit#90: Directly implements Phase 1 of the supply-chain integrity proposal—SHA-256 content hashing, well-known/lockfile persistence, and verify/publish/install integration.

Possibly related PRs

  • rohitg00/skillkit#36: Both modify WellKnownSkill and generateWellKnownIndex in packages/core/src/providers/wellknown.ts; this PR extends the schema to include integrity field.
  • rohitg00/skillkit#26: Both update COMMAND_GROUPS in docs/skillkit/components/Commands.tsx; this PR adds verify command entries.
  • rohitg00/skillkit#105: Both touch lockfile/checksum plumbing in install/update/remove flows that persist integrity digests.

Poem

🐰 Integrity verified, no fluff!

Files hashed in order, SHA-256 clean,
From publish to lock, a digest unseen,
skillkit verify stands tall and keen,
Supply-chain defense, pure and keen! 📦✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat(integrity): SHA-256 skill integrity verification (#90 phase 1)' accurately summarizes the main change: implementing SHA-256-based integrity verification for skills with support for the verify command, publish integration, lockfile recording, and documentation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/skill-integrity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CHANGELOG.md (1)

25-25: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove stray trailing content.

Line 25 appears to contain an orphan 25, which looks like an accidental artifact in the markdown file.

🤖 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 25, Remove the stray orphan token "25" found in the
changelog content (it appears as accidental trailing content) by deleting that
character so the CHANGELOG.md contains only valid markdown entries; ensure no
other extraneous characters remain and validate the file renders correctly after
removing the orphan "25".
🧹 Nitpick comments (3)
packages/core/src/integrity/integrity.ts (2)

97-97: ⚡ Quick win

Consider using basename for clearer filename extraction.

The expression file.split(sep).pop()! works correctly but using basename(file) from the already-imported node:path module would be more idiomatic and eliminate the non-null assertion.

♻️ Proposed refactor

Update the import on line 3:

-import { join, relative, sep, posix } from 'node:path';
+import { join, relative, sep, posix, basename } from 'node:path';

Then update line 97:

-    const rel = stats.isFile() ? toPosix(file.split(sep).pop()!) : toPosix(relative(root, file));
+    const rel = stats.isFile() ? toPosix(basename(file)) : toPosix(relative(root, file));
🤖 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 `@packages/core/src/integrity/integrity.ts` at line 97, The filename extraction
in the integrity computation uses file.split(sep).pop()! which is brittle and
uses a non-null assertion; replace that with the path.basename utility: import
basename from node:path (or add basename to the existing path import) and change
the expression in the const rel assignment (the line assigning rel in
integrity.ts) to use basename(file) wrapped in toPosix, removing the non-null
assertion. This updates the import list to include basename and makes the
filename extraction idiomatic and safe.

45-45: ⚡ Quick win

Consider locale-independent sorting for full cross-platform determinism.

Using localeCompare() without a locale argument may produce different sort orders for filenames with non-ASCII characters across different system locales. While ASCII filenames (the common case) sort consistently, specifying a fixed locale ensures identical integrity digests across all environments.

♻️ Proposed change for deterministic sorting
-  for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
+  for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en-US'))) {

Or use plain comparison for full independence:

-  for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
+  for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
🤖 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 `@packages/core/src/integrity/integrity.ts` at line 45, The directory-entry
sort uses a.name.localeCompare(b.name) which depends on system locale; change
the comparator in the sort call (the anonymous comparator passed to
readdirSync(...).sort(...)) to a deterministic, locale-independent comparison —
either use localeCompare with an explicit fixed locale (e.g.,
localeCompare(b.name, 'en') or similar) or replace with a plain bytewise
comparison like (a.name > b.name) ? 1 : (a.name < b.name) ? -1 : 0 so filename
ordering (and resulting integrity digests) is identical across environments.
packages/cli/src/__tests__/verify.test.ts (1)

77-81: ⚡ Quick win

Assert JSON reason for invalid expected format.

This case only checks exit code. Since reason codes are part of the CLI contract, assert invalid-expected-format here too.

Proposed test tightening
   it('rejects garbage --expected', () => {
     writeFileSync(join(dir, 'SKILL.md'), 'x');
-    const r = run(['verify', dir, '--expected', 'garbage']);
+    const r = run(['verify', dir, '--expected', 'garbage', '--json']);
     expect(r.code).toBe(1);
+    const payload = JSON.parse(r.stdout);
+    expect(payload.ok).toBe(false);
+    expect(payload.reason).toBe('invalid-expected-format');
   });
🤖 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 `@packages/cli/src/__tests__/verify.test.ts` around lines 77 - 81, The test
'rejects garbage --expected' only checks exit code; update it to also parse the
CLI JSON output and assert that the error reason equals
"invalid-expected-format". After running run(['verify', dir, '--expected',
'garbage']) and checking r.code === 1, parse r.stdout or r.stderr (whichever the
CLI emits JSON to) into an object and assert obj.reason ===
'invalid-expected-format' so the test verifies the CLI contract; reference the
existing test name and the variable r used to capture the run result.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@CHANGELOG.md`:
- 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.

In `@docs/skillkit/components/Commands.tsx`:
- Around line 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).

In `@packages/cli/src/commands/verify.ts`:
- Around line 93-123: The code currently calls verifySkillIntegrity(target,
expected) which recomputes digests; instead reuse the previously computed
integrity object (computed) from computeSkillIntegrity(target) to determine
validity and metadata so we don't hit disk twice. Change the call site in the
verify flow to either (A) call a new/overloaded verifySkillIntegrity(expected,
computed) that accepts the precomputed result, or (B) replace the call with a
simple comparison using parsed.digest vs computed.digest/SRI to produce the same
result shape (valid, algorithm, computed, reason), and then use that result for
the JSON/console output (ensure you reference parsed, computed, and result
fields consistently).
- Around line 72-92: If the user passed --expected but it is empty/whitespace,
and when parseIntegrity(expected) fails, we must treat that as a user-provided
invalid expected and emit structured JSON when this.json is true. Update the
logic around expectedFromArgs/expected to: detect when this.expected !==
undefined but expectedFromArgs === '' and handle it like an invalid expected
(return 1); and when parsed is falsy, replace the plain error(...) call with
conditional JSON output (this.context.stdout.write(JSON.stringify({ ok: false,
reason: 'invalid-expected-format', target }, null, 2) + '\n')) when this.json is
true, otherwise call error(...) as before; use the existing symbols
expectedFromArgs, expectedFromLock, expected, parseIntegrity, this.json, error,
warn and target to locate and modify the checks.

---

Outside diff comments:
In `@CHANGELOG.md`:
- Line 25: Remove the stray orphan token "25" found in the changelog content (it
appears as accidental trailing content) by deleting that character so the
CHANGELOG.md contains only valid markdown entries; ensure no other extraneous
characters remain and validate the file renders correctly after removing the
orphan "25".

---

Nitpick comments:
In `@packages/cli/src/__tests__/verify.test.ts`:
- Around line 77-81: The test 'rejects garbage --expected' only checks exit
code; update it to also parse the CLI JSON output and assert that the error
reason equals "invalid-expected-format". After running run(['verify', dir,
'--expected', 'garbage']) and checking r.code === 1, parse r.stdout or r.stderr
(whichever the CLI emits JSON to) into an object and assert obj.reason ===
'invalid-expected-format' so the test verifies the CLI contract; reference the
existing test name and the variable r used to capture the run result.

In `@packages/core/src/integrity/integrity.ts`:
- Line 97: The filename extraction in the integrity computation uses
file.split(sep).pop()! which is brittle and uses a non-null assertion; replace
that with the path.basename utility: import basename from node:path (or add
basename to the existing path import) and change the expression in the const rel
assignment (the line assigning rel in integrity.ts) to use basename(file)
wrapped in toPosix, removing the non-null assertion. This updates the import
list to include basename and makes the filename extraction idiomatic and safe.
- Line 45: The directory-entry sort uses a.name.localeCompare(b.name) which
depends on system locale; change the comparator in the sort call (the anonymous
comparator passed to readdirSync(...).sort(...)) to a deterministic,
locale-independent comparison — either use localeCompare with an explicit fixed
locale (e.g., localeCompare(b.name, 'en') or similar) or replace with a plain
bytewise comparison like (a.name > b.name) ? 1 : (a.name < b.name) ? -1 : 0 so
filename ordering (and resulting integrity digests) is identical across
environments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa5b3d0a-b922-4f2b-8202-92b176e89637

📥 Commits

Reviewing files that changed from the base of the PR and between a9e5c83 and 1436180.

📒 Files selected for processing (20)
  • CHANGELOG.md
  • README.md
  • apps/skillkit/src/cli.ts
  • docs/fumadocs/content/docs/commands.mdx
  • docs/fumadocs/content/docs/index.mdx
  • docs/fumadocs/content/docs/security.mdx
  • docs/skillkit/components/Commands.tsx
  • docs/skillkit/components/Features.tsx
  • packages/cli/src/__tests__/commands.test.ts
  • packages/cli/src/__tests__/verify.test.ts
  • packages/cli/src/commands/index.ts
  • packages/cli/src/commands/install.ts
  • packages/cli/src/commands/publish.ts
  • packages/cli/src/commands/update.ts
  • packages/cli/src/commands/verify.ts
  • packages/core/src/index.ts
  • packages/core/src/integrity/__tests__/integrity.test.ts
  • packages/core/src/integrity/index.ts
  • packages/core/src/integrity/integrity.ts
  • packages/core/src/providers/wellknown.ts

Comment thread CHANGELOG.md
- 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.

Comment on lines +103 to +104
{ cmd: 'verify --expected sha256-...', desc: 'Match publisher digest' },
{ cmd: 'verify --against-lock', desc: 'Verify vs lockfile' },
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).

Comment on lines +72 to +92
const expectedFromArgs = this.expected?.trim();
const expectedFromLock = this.againstLock ? findLockChecksum(target) : null;
const expected = expectedFromArgs ?? expectedFromLock ?? null;

if (this.againstLock && !expectedFromLock) {
if (this.json) {
this.context.stdout.write(
JSON.stringify({ ok: false, reason: 'no-lock-entry', target }, null, 2) + '\n',
);
} else {
warn(`No lock entry found for ${target}`);
}
return 1;
}

if (expected) {
const parsed = parseIntegrity(expected);
if (!parsed) {
error(`Invalid --expected value (must be sha256-<base64> or 64-char hex)`);
return 1;
}
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 | 🟠 Major | ⚡ Quick win

Honor --expected intent and return structured JSON reasons.

If --expected is provided but empty/whitespace, the command currently falls back to compute-only success. Also, invalid expected format emits plain text even with --json, which breaks CI reason-code handling.

💡 Suggested fix
-    const expectedFromArgs = this.expected?.trim();
+    const expectedFromArgsRaw = this.expected;
+    const expectedFromArgs = expectedFromArgsRaw?.trim();
+
+    if (expectedFromArgsRaw !== undefined && !expectedFromArgs) {
+      if (this.json) {
+        this.context.stdout.write(
+          JSON.stringify({ ok: false, reason: 'missing-expected', target }, null, 2) + '\n',
+        );
+      } else {
+        error(`Missing --expected value`);
+      }
+      return 1;
+    }
@@
     if (expected) {
       const parsed = parseIntegrity(expected);
       if (!parsed) {
-        error(`Invalid --expected value (must be sha256-<base64> or 64-char hex)`);
+        if (this.json) {
+          this.context.stdout.write(
+            JSON.stringify({ ok: false, reason: 'invalid-expected-format', target }, null, 2) + '\n',
+          );
+        } else {
+          error(`Invalid --expected value (must be sha256-<base64> or 64-char hex)`);
+        }
         return 1;
       }
🤖 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 `@packages/cli/src/commands/verify.ts` around lines 72 - 92, If the user passed
--expected but it is empty/whitespace, and when parseIntegrity(expected) fails,
we must treat that as a user-provided invalid expected and emit structured JSON
when this.json is true. Update the logic around expectedFromArgs/expected to:
detect when this.expected !== undefined but expectedFromArgs === '' and handle
it like an invalid expected (return 1); and when parsed is falsy, replace the
plain error(...) call with conditional JSON output
(this.context.stdout.write(JSON.stringify({ ok: false, reason:
'invalid-expected-format', target }, null, 2) + '\n')) when this.json is true,
otherwise call error(...) as before; use the existing symbols expectedFromArgs,
expectedFromLock, expected, parseIntegrity, this.json, error, warn and target to
locate and modify the checks.

Comment on lines +93 to +123
const result = verifySkillIntegrity(target, expected);
if (this.json) {
this.context.stdout.write(
JSON.stringify(
{
ok: result.valid,
algorithm: result.algorithm,
expected: parsed.digest,
computed: result.computed,
sri: computed.sri,
files: computed.files.length,
totalBytes: computed.totalBytes,
reason: result.reason,
},
null,
2,
) + '\n',
);
return result.valid ? 0 : 1;
}
if (result.valid) {
success(`Integrity verified (${computed.files.length} files, ${computed.totalBytes} bytes)`);
this.context.stdout.write(` ${colors.dim('expected')} ${parsed.digest}\n`);
this.context.stdout.write(` ${colors.dim('computed')} ${result.computed}\n`);
return 0;
}
error('Integrity mismatch — skill content has changed since signing');
this.context.stdout.write(` ${colors.dim('expected')} ${parsed.digest}\n`);
this.context.stdout.write(` ${colors.dim('computed')} ${result.computed}\n`);
return 1;
}
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 | 🟠 Major | ⚡ Quick win

Avoid recomputing integrity during verification.

verifySkillIntegrity(target, expected) recomputes the digest after computeSkillIntegrity(target) already ran. This adds redundant disk I/O and can make reported fields inconsistent if files change between reads.

💡 Suggested fix
-      const result = verifySkillIntegrity(target, expected);
+      const isValid = parsed.digest === computed.digest;
+      const reason = isValid ? undefined : 'mismatch';
       if (this.json) {
         this.context.stdout.write(
           JSON.stringify(
             {
-              ok: result.valid,
-              algorithm: result.algorithm,
+              ok: isValid,
+              algorithm: computed.algorithm,
               expected: parsed.digest,
-              computed: result.computed,
+              computed: computed.digest,
               sri: computed.sri,
               files: computed.files.length,
               totalBytes: computed.totalBytes,
-              reason: result.reason,
+              reason,
             },
             null,
             2,
           ) + '\n',
         );
-        return result.valid ? 0 : 1;
+        return isValid ? 0 : 1;
       }
-      if (result.valid) {
+      if (isValid) {
         success(`Integrity verified (${computed.files.length} files, ${computed.totalBytes} bytes)`);
         this.context.stdout.write(`  ${colors.dim('expected')}  ${parsed.digest}\n`);
-        this.context.stdout.write(`  ${colors.dim('computed')}  ${result.computed}\n`);
+        this.context.stdout.write(`  ${colors.dim('computed')}  ${computed.digest}\n`);
         return 0;
       }
       error('Integrity mismatch — skill content has changed since signing');
       this.context.stdout.write(`  ${colors.dim('expected')}  ${parsed.digest}\n`);
-      this.context.stdout.write(`  ${colors.dim('computed')}  ${result.computed}\n`);
+      this.context.stdout.write(`  ${colors.dim('computed')}  ${computed.digest}\n`);
       return 1;
📝 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
const result = verifySkillIntegrity(target, expected);
if (this.json) {
this.context.stdout.write(
JSON.stringify(
{
ok: result.valid,
algorithm: result.algorithm,
expected: parsed.digest,
computed: result.computed,
sri: computed.sri,
files: computed.files.length,
totalBytes: computed.totalBytes,
reason: result.reason,
},
null,
2,
) + '\n',
);
return result.valid ? 0 : 1;
}
if (result.valid) {
success(`Integrity verified (${computed.files.length} files, ${computed.totalBytes} bytes)`);
this.context.stdout.write(` ${colors.dim('expected')} ${parsed.digest}\n`);
this.context.stdout.write(` ${colors.dim('computed')} ${result.computed}\n`);
return 0;
}
error('Integrity mismatch — skill content has changed since signing');
this.context.stdout.write(` ${colors.dim('expected')} ${parsed.digest}\n`);
this.context.stdout.write(` ${colors.dim('computed')} ${result.computed}\n`);
return 1;
}
const isValid = parsed.digest === computed.digest;
const reason = isValid ? undefined : 'mismatch';
if (this.json) {
this.context.stdout.write(
JSON.stringify(
{
ok: isValid,
algorithm: computed.algorithm,
expected: parsed.digest,
computed: computed.digest,
sri: computed.sri,
files: computed.files.length,
totalBytes: computed.totalBytes,
reason,
},
null,
2,
) + '\n',
);
return isValid ? 0 : 1;
}
if (isValid) {
success(`Integrity verified (${computed.files.length} files, ${computed.totalBytes} bytes)`);
this.context.stdout.write(` ${colors.dim('expected')} ${parsed.digest}\n`);
this.context.stdout.write(` ${colors.dim('computed')} ${computed.digest}\n`);
return 0;
}
error('Integrity mismatch — skill content has changed since signing');
this.context.stdout.write(` ${colors.dim('expected')} ${parsed.digest}\n`);
this.context.stdout.write(` ${colors.dim('computed')} ${computed.digest}\n`);
return 1;
🤖 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 `@packages/cli/src/commands/verify.ts` around lines 93 - 123, The code
currently calls verifySkillIntegrity(target, expected) which recomputes digests;
instead reuse the previously computed integrity object (computed) from
computeSkillIntegrity(target) to determine validity and metadata so we don't hit
disk twice. Change the call site in the verify flow to either (A) call a
new/overloaded verifySkillIntegrity(expected, computed) that accepts the
precomputed result, or (B) replace the call with a simple comparison using
parsed.digest vs computed.digest/SRI to produce the same result shape (valid,
algorithm, computed, reason), and then use that result for the JSON/console
output (ensure you reference parsed, computed, and result fields consistently).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant