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:

  1. Downloads the latest skill files
  2. Recomputes the skillFolderHash from the new tree
  3. 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 skillFolderHash is a GitHub tree SHA that fingerprint the entire contents of a skill folder in a single value.

  • Storage location: ~/.agents/.skill-lock.json, managed through readSkillLock() and writeSkillLock() in src/skill-lock.ts.

  • Remote fetch: fetchSkillFolderHash() in src/skill-lock.ts delegates to fetchRepoTree() and getSkillFolderHashFromTree() in src/blob.ts, which query the GitHub Trees API.

  • Change detection: updateGlobalSkills() in src/cli.ts compares 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →