feat(skills): report hash drift in skill info#795
Conversation
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.
WalkthroughAdds ChangesSkill hash drift reporting
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
internal/cli/distribution_test.go (1)
86-101: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAdd JSON regression assertions.
This test only exercises human-readable output, while the PR also promises
hashDriftin JSON. Check bothfalsebefore editing andtrueafter editing; otherwise theomitemptyregression 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
📒 Files selected for processing (5)
docs/EXTENDING.mdinternal/cli/distribution.gointernal/cli/distribution_test.gointernal/skills/install.gointernal/skills/install_test.go
| 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. |
There was a problem hiding this comment.
🎯 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"` |
There was a problem hiding this comment.
🗄️ 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.modRepository: 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:
- 1: https://pkg.go.dev/encoding/json
- 2: https://github.com/golang/go/blob/2403e594/src/encoding/json/v2_options.go
- 3: https://github.com/golang/go/blob/master/src/encoding/json/v2_encode.go
- 4: encoding/json: Marshaller omits false booleans as if they were nil golang/go#13284
- 5: fix: remove omitempty from plain bool JSON tags — false silently dropped cdot65/prisma-airs-go#87
- 6: https://navendu.me/tils/9-5-25-omitempty-option-omits-boolean-fields-with-false-values/
- 7: https://stackoverflow.com/questions/37756236/json-golang-boolean-omitempty
- 8: https://www.sohamkamani.com/golang/omitempty/
- 9: https://pkg.go.dev/encoding/json/v2
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.
|
|
||
| // 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 | ||
| } |
There was a problem hiding this comment.
🗄️ 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 || trueRepository: 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)))
PYRepository: 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.
Summary
SKILL.mdno longer matchesskills.lockhashDriftinzero skill infotext + JSON (same idea as plugins info)skills lockcommandCloses #792
Test plan
go test ./internal/skills/ ./internal/cli/ -run 'Info|Skill|HashDrift|RunSkill' -count=1go vet ./internal/skills/ ./internal/cli/Summary by CodeRabbit
New Features
skill info, showing whether an installed skill differs from its recorded source.Bug Fixes