How skillFolderHash Detects Skill Updates in the Vercel Skills CLI
The skillFolderHash is a GitHub tree SHA of the entire skill folder, stored in ~/.agents/.skill-lock.json and compared against the remote repository to detect changes without downloading individual files.
The Vercel Skills CLI uses a lightweight, efficient mechanism to determine when installed skills need updates. At the heart of this system is the skillFolderHash — a cryptographic fingerprint of the entire skill directory that enables change detection through a single API call. This article explains how the hash is generated, stored, and compared according to the vercel-labs/skills source code.
What Is skillFolderHash and Where Is It Stored
The skillFolderHash is the SHA-256 hash of a GitHub tree object representing the complete contents of a skill folder. GitHub generates this hash automatically when computing the tree structure of a repository, and it changes whenever any file within that directory is added, removed, or modified.
The CLI stores this hash in a global lock file located at:
~/.agents/.skill-lock.json
Each entry in this file follows the SkillLockEntry interface defined in src/skill-lock.ts:
{
source: string; // e.g., "vercel-labs/agent-skills"
skillPath?: string; // path to folder containing SKILL.md
skillFolderHash: string; // SHA of that folder in the repo
ref?: string; // branch or tag
// ...
}
How the CLI Fetches and Compares skillFolderHash Values
When you run skills check or skills update, the CLI executes a three-step comparison process to detect outdated skills.
Step 1: Read the Local Lock File
The readSkillLock() function in src/skill-lock.ts parses ~/.agents/.skill-lock.json and returns the stored skillFolderHash for each installed skill.
Step 2: Fetch the Latest Remote Hash
The fetchSkillFolderHash() function (also in src/skill-lock.ts) retrieves the current hash from GitHub:
const latestHash = await fetchSkillFolderHash(
entry.source, // "owner/repo"
entry.skillPath!, // folder path within repo
token, // GitHub token for API rate limits
entry.ref // branch or tag
);
This function delegates to src/blob.ts, which implements the GitHub Trees API integration:
const { fetchRepoTree, getSkillFolderHashFromTree } = await import('./blob.ts');
const tree = await fetchRepoTree(ownerRepo, ref, token);
return getSkillFolderHashFromTree(tree, skillPath);
Step 3: Fetch the Repository Tree
The fetchRepoTree() function in src/blob.ts makes a single authenticated request to:
GET https://api.github.com/repos/{ownerRepo}/git/trees/{branch}?recursive=1
The recursive=1 parameter returns the complete directory structure in one response, including all nested files and subdirectories. Each tree entry contains:
{
path: string; // file or folder path
mode: string; // file permissions
type: "blob" | "tree"; // file or directory
sha: string; // object hash
size?: number; // bytes (files only)
}
Step 4: Extract the Folder Hash
The getSkillFolderHashFromTree() function in src/blob.ts locates the specific skill folder within this tree:
function getSkillFolderHashFromTree(tree: GitTree, skillPath: string): string | null {
// Normalize backslashes to forward slashes
const normalizedPath = skillPath.replace(/\\/g, '/');
// Remove trailing SKILL.md if present
const folderPath = normalizedPath.replace(/\/SKILL.md$/, '').replace(/SKILL.md$/, '');
// Find the tree entry matching this folder path
const entry = tree.tree.find(e => e.type === 'tree' && e.path === folderPath);
return entry?.sha ?? null;
}
The function returns the sha of the tree object representing the skill folder, or null if the folder is not found.
How skillFolderHash Compare Enables Update Detection
The actual comparison occurs in updateGlobalSkills() within src/cli.ts. The CLI iterates through each installed skill and compares the stored hash against the freshly fetched remote hash:
// Pseudocode from updateGlobalSkills logic
for (const entry of skillLock.entries) {
const latestHash = await fetchSkillFolderHash(...);
if (latestHash !== entry.skillFolderHash) {
// Hash mismatch = folder contents changed
skillsToUpdate.push(entry);
}
}
When latestHash !== entry.skillFolderHash, the CLI knows that something within that skill folder has changed — whether a minor documentation edit, a new example file, or a structural refactor. This triggers the update queue.
Installing Updates and Refreshing the Hash
Once outdated skills are identified, the CLI performs updates by re-invoking the installation command:
skills add <source> -g -y
The -g flag ensures global installation, and -y auto-confirms the operation. This fresh installation:
- Downloads the latest skill files
- Recomputes the
skillFolderHashfrom the new tree - Writes the updated entry to
~/.agents/.skill-lock.json
Practical Example: Checking a Skill's Folder Hash
You can manually verify the hash mechanism using the internal functions:
import { fetchSkillFolderHash } from './src/skill-lock.ts';
// Check the hash for a specific skill
const ownerRepo = 'vercel-labs/agent-skills';
const skillPath = 'skills/react-best-practices'; // folder containing SKILL.md
const token = process.env.GITHUB_TOKEN; // recommended for API rate limits
const latestHash = await fetchSkillFolderHash(ownerRepo, skillPath, token);
console.log('Current folder hash:', latestHash);
Running this outputs the same SHA that appears in your lock file entry. If your local skillFolderHash differs, the CLI will flag this skill for update on the next skills check run.
Summary
-
The
skillFolderHashis a GitHub tree SHA that fingerprint the entire contents of a skill folder in a single value. -
Storage location:
~/.agents/.skill-lock.json, managed throughreadSkillLock()andwriteSkillLock()insrc/skill-lock.ts. -
Remote fetch:
fetchSkillFolderHash()insrc/skill-lock.tsdelegates tofetchRepoTree()andgetSkillFolderHashFromTree()insrc/blob.ts, which query the GitHub Trees API. -
Change detection:
updateGlobalSkills()insrc/cli.tscompares stored vs. remote hashes; mismatches trigger updates. -
Efficiency: One API call yields the entire repository tree, enabling change detection for all installed skills without downloading individual files.
Frequently Asked Questions
What exactly does skillFolderHash represent?
The skillFolderHash is the SHA-256 hash of a GitHub tree object that represents the complete directory structure and file contents of a skill folder. GitHub computes this hash automatically when indexing repository contents. Any change to any file within the folder — including metadata, documentation, or code — produces a different tree SHA, making the hash a reliable change detector.
Why does the CLI use a tree hash instead of file timestamps or individual file hashes?
The tree hash approach provides atomic, single-request change detection across an entire folder. The GitHub Trees API with ?recursive=1 returns every file and subdirectory in one response, eliminating the need for multiple API calls round-trips. This is particularly efficient for skills containing many files. Individual file hashes would require either caching complex state or querying each file separately, while timestamps are unreliable across distributed version control systems.
Where can I find the skillFolderHash for an installed skill?
The hash is stored in your global lock file at ~/.agents/.skill-lock.json. Each entry in this JSON file contains a skillFolderHash field alongside source, skillPath, and other metadata. You can view it directly with:
cat ~/.agents/.skill-lock.json | jq '.[] | select(.skillPath | contains("your-skill-name"))'
The CLI reads this value via readSkillLock() in src/skill-lock.ts during skills check and skills update operations.
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 →