Understanding the Lock File Architecture in the Skills CLI: Global `.skill-lock.json` vs Local `skills-lock.json`
The skills CLI uses two separate lock files—global .skill-lock.json for machine-wide update tracking and local skills-lock.json for project-specific reproducible installs.
The skills CLI from vercel-labs/skills implements a dual-lock architecture that separates global state management from project-level reproducibility. This design lets you track updates across your entire machine while keeping individual projects deterministic and merge-friendly.
Global Lock File: .skill-lock.json
The global lock file serves as the source of truth for CLI operations like skills check and skills update. It persists installation metadata across all projects on a machine.
Location and Environment Detection
The CLI resolves the global lock path using platform-aware logic:
# Default location
~/.agents/.skill-lock.json
# XDG-compliant alternative (when $XDG_STATE_HOME is set)
$XDG_STATE_HOME/skills/.skill-lock.json
Schema Version 3 Structure
Defined in src/skill-lock.ts, the global lock uses version 3 with rich metadata:
{
"version": 3,
"skills": {
"react-best-practices": {
"source": "vercel-labs/agent-skills",
"sourceType": "github",
"sourceUrl": "https://github.com/vercel-labs/agent-skills",
"skillPath": "skills/react-best-practices",
"skillFolderHash": "abc123def456...",
"pluginName": undefined,
"installedAt": "2024-01-15T09:23:17.000Z",
"updatedAt": "2024-01-15T09:23:17.000Z"
}
},
"dismissed": {},
"lastSelectedAgents": []
}
GitHub Tree SHA Hashing
The skillFolderHash field stores a GitHub tree SHA fetched via the GitHub Trees API. This approach detects any change in the remote repository folder without downloading content.
The implementation in src/blob.ts provides fetchRepoTree and getSkillFolderHashFromTree for this purpose.
Update Detection Logic
import { readSkillLock } from './skill-lock';
async function checkForUpdates(skillName: string) {
const lock = await readSkillLock();
const entry = lock.skills[skillName];
// Compare stored hash with fresh GitHub API call
const currentHash = await fetchSkillFolderHash(entry.source, entry.skillPath);
if (entry.skillFolderHash !== currentHash) {
console.log(`Update available for ${skillName}`);
}
}
UI State Persistence
The global lock includes dismissed (tracks UI prompts) and lastSelectedAgents (remembers user selections), keeping machine-specific state out of version control.
Local Lock File: skills-lock.json
The local lock file provides project-scoped metadata designed for version control. It ensures reproducible installs without the merge conflicts that timestamps and UI state would cause.
Location and Design Philosophy
./skills-lock.json (in project root, committed to git)
The design deliberately excludes timestamps and machine-specific state to keep diffs minimal and deterministic.
Schema Version 1 Structure
Defined in src/local-lock.ts, the local lock uses a minimal version 1 schema:
{
"version": 1,
"skills": {
"react-best-practices": {
"source": "vercel-labs/agent-skills",
"sourceType": "github",
"ref": "main",
"computedHash": "deadbeef0123456789abcdef..."
}
}
}
Local Content Hashing
Unlike the global lock's GitHub tree SHA, the local lock stores a computed SHA-256 hash of the actual files on disk. This computedHash is calculated from the skill folder contents after installation, making it independent of remote state.
import { computeSkillFolderHash } from './hash';
// SHA-256 of local file contents
const computedHash = await computeSkillFolderHash('/path/to/skill/folder');
Project Reproducibility
The local lock enables deterministic reinstalls:
import { readLocalLock, addSkillToLocalLock } from './local-lock';
async function reproduceInstall(projectPath: string) {
const lock = await readLocalLock(projectPath);
for (const [name, entry] of Object.entries(lock.skills)) {
// Reinstall using exact ref and verify against computedHash
await installSkill(entry.source, entry.ref, {
verifyHash: entry.computedHash
});
}
}
Comparing Global and Local Lock Approaches
| Aspect | Global .skill-lock.json |
Local skills-lock.json |
|---|---|---|
| File path | ~/.agents/.skill-lock.json or $XDG_STATE_HOME/skills/.skill-lock.json |
./skills-lock.json in project root |
| Source file | src/skill-lock.ts |
src/local-lock.ts |
| Schema version | version: 3 |
version: 1 |
| Hash type | GitHub tree SHA (remote state) | SHA-256 of local file contents |
| Timestamps | installedAt, updatedAt |
None (deterministic) |
| UI state | dismissed, lastSelectedAgents |
None |
| Git strategy | .gitignore (machine-specific) |
Committed to version control |
| Primary use | Update detection, global state | Reproducible installs, CI/CD |
Key Implementation Files
| File | Responsibility |
|---|---|
src/skill-lock.ts |
Global lock file operations, version 3 schema, GitHub tree hash integration |
src/local-lock.ts |
Local lock file operations, version 1 schema, content hash verification |
src/blob.ts |
fetchRepoTree and getSkillFolderHashFromTree for GitHub API calls |
src/hash.ts |
computeSkillFolderHash for local SHA-256 calculation |
src/add.ts |
Writes both lock files during skill installation |
src/cli.ts |
skills check and skills update commands using global lock data |
Summary
-
The global
.skill-lock.jsontracks machine-wide skill installations withversion: 3, GitHub tree SHA hashes, and timestamps for update detection. -
The local
skills-lock.jsonprovides project-scoped reproducibility withversion: 1, content-based SHA-256 hashes, and no timestamps for merge-friendly version control. -
Both files are written together during
skills add, but serve fundamentally different purposes: global state persistence versus deterministic project setup.
Frequently Asked Questions
What happens if I delete the global .skill-lock.json?
The CLI will recreate it on the next operation, but you will lose update tracking history and UI state like dismissed prompts. Your installed skills remain in ~/.agents/, but the CLI won't know when they were last checked for updates.
Should I commit skills-lock.json to my repository?
Yes. The local lock file is designed specifically for version control. Its deterministic, timestamp-free format ensures reproducible installs across environments and avoids merge conflicts that timestamps would cause.
Why does the global lock use GitHub tree SHA while the local lock uses content hash?
The global lock needs to detect remote changes without downloading content, so it stores the GitHub tree SHA fetched via API. The local lock needs to verify the actual files on disk, so it computes a SHA-256 hash from local content.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →