Skip to content

feat(skills): report hash drift in skill info#795

Open
Ayush7614 wants to merge 1 commit into
Gitlawb:mainfrom
Ayush7614:feat/skills-info-hash-drift
Open

feat(skills): report hash drift in skill info#795
Ayush7614 wants to merge 1 commit into
Gitlawb:mainfrom
Ayush7614:feat/skills-info-hash-drift

Conversation

@Ayush7614

@Ayush7614 Ayush7614 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Detect when an installed skill's SKILL.md no longer matches skills.lock
  • Surface hashDrift in zero skill info text + JSON (same idea as plugins info)
  • Fix EXTENDING.md docs that claimed a nonexistent skills lock command

Closes #792

Test plan

  • go test ./internal/skills/ ./internal/cli/ -run 'Info|Skill|HashDrift|RunSkill' -count=1
  • go vet ./internal/skills/ ./internal/cli/

Summary by CodeRabbit

  • New Features

    • Added hash drift reporting to skill info, showing whether an installed skill differs from its recorded source.
    • Documented skill directory behavior and hash drift details.
  • Bug Fixes

    • Skill management commands now consistently write to the primary skills directory.
    • Improved detection of changes to installed skill files.

Mirror plugin info: compare on-disk SKILL.md to the skills.lock hash so
edited installs are visible. Also drop the nonexistent skills lock
command from EXTENDING.md.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds HashDrift detection for installed skills by comparing SKILL.md with the lockfile hash, exposes the result in skill info, tests clean and modified states, and updates skills-directory documentation.

Changes

Skill hash drift reporting

Layer / File(s) Summary
Skill metadata drift detection
internal/skills/install.go, internal/skills/install_test.go
SkillInfo now includes HashDrift; Info and InfoFromRoots compare installed SKILL.md content with the recorded lock hash and tests cover clean and modified content.
CLI output and documentation
internal/cli/distribution.go, internal/cli/distribution_test.go, docs/EXTENDING.md
skill info prints hash drift status, CLI tests verify both boolean values, and skills-directory behavior documentation is corrected.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SkillCLI
  participant SkillInfo
  participant SkillFiles
  User->>SkillCLI: run skill info
  SkillCLI->>SkillInfo: request skill metadata
  SkillInfo->>SkillFiles: read SKILL.md and lock hash
  SkillFiles-->>SkillInfo: return content and recorded hash
  SkillInfo-->>SkillCLI: return HashDrift
  SkillCLI-->>User: print hash drift status
Loading

Possibly related PRs

Suggested reviewers: kevincodex1, vasanthdev2004

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: reporting hash drift for skill info.
Linked Issues check ✅ Passed The PR implements hashDrift computation, updates text/JSON output, and fixes EXTENDING.md as requested by #792.
Out of Scope Changes check ✅ Passed The changes stay within the skill info, docs, and test updates required by the linked issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/cli/distribution_test.go (1)

86-101: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Add JSON regression assertions.

This test only exercises human-readable output, while the PR also promises hashDrift in JSON. Check both false before editing and true after editing; otherwise the omitempty regression can pass unnoticed.

As per coding guidelines, add regression tests for behavior changes and keep them beside the source.

🤖 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 `@internal/cli/distribution_test.go` around lines 86 - 101, Extend the
distribution regression test around runWithDeps and the skill info command to
request JSON output and assert hashDrift is false before editing the skill, then
true after editing it. Parse the JSON response and verify the field is present
with the correct boolean value, while preserving the existing human-readable
assertions.

Source: Coding guidelines

🤖 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 `@docs/EXTENDING.md`:
- Line 111: Update the management-command examples in the documentation to use
the actual singular `zero skill add` and `zero skill remove` names, while
preserving the existing descriptions of their directory behavior.

In `@internal/skills/install.go`:
- Around line 504-518: Update skillHashDrift and its callers in
Info/InfoFromRoots so a locked skill whose os.ReadFile(skill.Path) fails is
surfaced as an error or marked as HashDrift=true, rather than returning a clean
false state; preserve the existing behavior for empty lock hashes and
successfully read files.
- Line 82: Update the HashDrift field’s JSON serialization so a computed
unchanged result is emitted as hashDrift: false instead of being omitted; remove
omitempty from its json tag, or use an equivalent pointer/custom-marshaler
representation if the no-lock “not computed” state must remain distinguishable.

---

Nitpick comments:
In `@internal/cli/distribution_test.go`:
- Around line 86-101: Extend the distribution regression test around runWithDeps
and the skill info command to request JSON output and assert hashDrift is false
before editing the skill, then true after editing it. Parse the JSON response
and verify the field is present with the correct boolean value, while preserving
the existing human-readable assertions.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 734c8dff-6e45-46b1-bcbe-cc08571f7df2

📥 Commits

Reviewing files that changed from the base of the PR and between d9b882e and 2531c39.

📒 Files selected for processing (5)
  • docs/EXTENDING.md
  • internal/cli/distribution.go
  • internal/cli/distribution_test.go
  • internal/skills/install.go
  • internal/skills/install_test.go

Comment thread docs/EXTENDING.md
3. **Plugin skill roots** — skills bundled by active plugins (section 6)

A missing directory is fine — Zero just omits it. Management commands (`zero skills add` / `remove` / `lock`) always write to the primary Zero dir only; `list` / `info` search primary + `~/.agents/skills`.
A missing directory is fine — Zero just omits it. Management commands (`zero skills add` / `remove`) always write to the primary Zero dir only; `list` / `info` search primary + `~/.agents/skills`. `skill info` reports lock source/hash when present and whether the on-disk `SKILL.md` has drifted from the recorded hash.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the actual singular CLI command name.

The supplied CLI and tests use skill add/skill info; zero skills add is misleading and may not exist. Document zero skill add / remove instead.

🤖 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/EXTENDING.md` at line 111, Update the management-command examples in the
documentation to use the actual singular `zero skill add` and `zero skill
remove` names, while preserving the existing descriptions of their directory
behavior.

Skill Skill `json:"skill"`
Source string `json:"source,omitempty"`
Hash string `json:"hash,omitempty"`
HashDrift bool `json:"hashDrift,omitempty"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'install.go$' . | sed 's#^\./##'

echo "== go.mod version =="
sed -n '1,80p' go.mod 2>/dev/null | rg 'go |module' || true

echo "== internal/skills/install.go outline =="
ast-grep outline internal/skills/install.go --view compact 2>/dev/null || true

echo "== relevant install.go lines =="
cat -n internal/skills/install.go | sed -n '1,180p'

echo "== searches for hashDrift/HashDrift =="
rg -n "HashDrift|hashDrift|skillHashDrift|InfoFromRoots|Info\\(" internal go.mod

Repository: Gitlawb/zero

Length of output: 13553


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== internal/skills/install.go Info section =="
cat -n internal/skills/install.go | sed -n '230,310p'

echo "== skills.json serialization tests covering HashDrift =="
rg -n "HashDrift|hashDrift|skills\\.json|Info\\(" internal/skills/install_test.go internal/skills
cat -n internal/skills/install_test.go | sed -n '350,430p'

echo "== distribution CLI formatting section =="
cat -n internal/cli/distribution.go | sed -n '110,160p'

echo "== plugin parallel implementation =="
cat -n internal/plugins/info.go | sed -n '1,90p'

Repository: Gitlawb/zero

Length of output: 11729


🌐 Web query:

Go encoding/json omitempty bool false omits field documentation

💡 Result:

In Go's encoding/json package, the omitempty struct tag option causes a field to be omitted from the JSON output if the field's value is the "zero value" for its type [1][2][3]. For a boolean type, the zero value is false [4][5][6]. Consequently, when a struct field defined as bool has the json:",omitempty" tag, an explicit false value will cause that field to be excluded entirely from the marshaled JSON [5][6]. To distinguish between a field being "unset" (omitted) and explicitly set to false, the standard practice is to use a pointer to a boolean (*bool) [4][7][6]. How to differentiate: 1. Plain bool with omitempty: If set to false, the field is omitted [5][6]. 2. Pointer to bool (*bool) with omitempty: - If the pointer is nil, the field is omitted [7][8]. - If the pointer is not nil (pointing to either true or false), the field is included in the JSON, allowing for explicit false values [7][6]. In the newer encoding/json/v2, the omitempty behavior has been refined to focus on whether a value encodes to an "empty" JSON value (like null or an empty object/array), while a new tag option, omitzero, is introduced to explicitly handle cases where the zero Go value should cause omission [1][2][9]. Developers are encouraged to migrate to omitzero for cases where the specific Go zero value (like false for bool) should trigger omission [1][2].

Citations:


Keep an unchanged hash-drift result in the JSON output.

HashDrift bool json:"hashDrift,omitempty" drops hashDrift when the skill matches its lock, so consumers cannot distinguish an unchanged install from a missing field. Remove omitempty, or use a pointer/custom marshaler for the “not computed because no lock exists” case.

🤖 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 `@internal/skills/install.go` at line 82, Update the HashDrift field’s JSON
serialization so a computed unchanged result is emitted as hashDrift: false
instead of being omitted; remove omitempty from its json tag, or use an
equivalent pointer/custom-marshaler representation if the no-lock “not computed”
state must remain distinguishable.

Comment on lines +504 to +518

// skillHashDrift reports whether the on-disk SKILL.md no longer matches the
// lockfile hash recorded at install time. Missing lock hashes never count as
// drift (agents-only / unlocked skills).
func skillHashDrift(skill Skill, lockHash string) bool {
lockHash = strings.TrimSpace(lockHash)
if lockHash == "" {
return false
}
data, err := os.ReadFile(skill.Path)
if err != nil {
return false
}
return hashContent(data) != lockHash
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== go.mod version =="
if [ -f go.mod ]; then awk '/^go /{print; exit}' go.mod; else echo "no go.mod"; fi

echo "== locate install.go and related files =="
fd -a 'install\.go$|skill|lock' . | sed 's#^\./##' | head -200

echo "== relevant excerpt =="
sed -n '460,545p' internal/skills/install.go

echo "== nearby callers/usages of skillHashDrift and error handling =="
rg -n "skillHashDrift|HashContent|hashContent|lockHash|SKILL\.md|ReadFile" internal/skills install.go go.mod 2>/dev/null || true

Repository: Gitlawb/zero

Length of output: 9367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== install.go structural outline =="
ast-grep outline internal/skills/install.go || true

echo "== Install, Info, and drift context =="
sed -n '80,330p' internal/skills/install.go

echo "== drift tests =="
rg -n "HashDrift|hash drift|skillHashDrift|Lock" internal/skills/install_test.go internal/skills/skills_test.go -C 3 || true

echo "== read-only structural/behavioral probe for current drift function =="
python3 - <<'PY'
from pathlib import Path
import re, html

src = Path("internal/skills/install.go").read_text()
m = re.search(r'func skillHashDrift\(skill Skill, lockHash string\) bool \{(?P<body>.*?)\n\}', src, re.S)
body = m.group("body") if m else ""
print("body:", body)
print("returns_false_on_read_error:", "if err != nil" in body and "return false" in re.search(r'if err != nil \{(?P<block>.*?)\n\t\}', body, re.S).group("block") if re.search(r'if err != nil \{(?P<block>.*?)\n\t\}', body, re.S) else None)
print("calls_hashContent_before_return_if_read_ok:", bool(re.search(r'data, err := os\.ReadFile\(skill\.Path\)\n\s*if err != nil \{\s*return false\s*\}\n\s*return hashContent\(data\)', body)))
PY

Repository: Gitlawb/zero

Length of output: 16127


Do not treat ReadFile errors as clean hash state.

Info/InfoFromRoots set HashDrift from skillHashDrift; returning false when os.ReadFile(skill.Path) fails makes unreadable locked SKILL.md files report false instead of surfacing the failure. Make skillHashDrift return an error/state or treat locked unreadable skills as drifted.

🤖 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 `@internal/skills/install.go` around lines 504 - 518, Update skillHashDrift and
its callers in Info/InfoFromRoots so a locked skill whose
os.ReadFile(skill.Path) fails is surfaced as an error or marked as
HashDrift=true, rather than returning a clean false state; preserve the existing
behavior for empty lock hashes and successfully read files.

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.

feat(skills): report hash drift in skill info

1 participant