How the SkillFolderHash Mechanism Works for Checking Skill Updates in Vercel Skills

The skillFolderHash mechanism uses a GitHub tree SHA to efficiently detect when any file inside a skill folder has changed, avoiding costly per-file comparisons.

The Vercel Skills CLI maintains a lightweight update detection system centered on a single hash value stored for each installed skill. This article explains how skillFolderHash works, walking through the source code in vercel-labs/skills to show exactly how the CLI determines when skills need updating.

What Is skillFolderHash?

skillFolderHash is a GitHub tree SHA representing the complete state of a skill's folder in its source repository. Rather than tracking individual files, the CLI stores one hash that changes if anything inside the folder is modified—files added, removed, or edited.

The hash lives in a global lock file at ~/.agents/.skill-lock.json. Each entry 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

When you run skills check or skills update, the CLI executes a three-step process to detect outdated skills.

Step 1: Read the Stored Hash from skill-lock.ts

The readSkillLock() function in src/skill-lock.ts loads the global lock file and returns all installed skill entries with their cached skillFolderHash values.

// From src/skill-lock.ts
import { readSkillLock } from './skill-lock.ts';

const lock = await readSkillLock();
const entry = lock.find(e => e.source === 'vercel-labs/agent-skills');
console.log(entry.skillFolderHash); // e.g. "abc123..."

Step 2: Fetch the Latest Hash via GitHub's Trees API

The fetchSkillFolderHash() function in src/skill-lock.ts retrieves the current folder hash from GitHub. It delegates to src/blob.ts for the actual API interaction:

// From src/skill-lock.ts
export async function fetchSkillFolderHash(
  ownerRepo: string,
  skillPath: string,
  token?: string,
  ref?: string
): Promise<string | null> {
  const { fetchRepoTree, getSkillFolderHashFromTree } = await import('./blob.ts');
  const tree = await fetchRepoTree(ownerRepo, ref, token);
  return getSkillFolderHashFromTree(tree, skillPath);
}

The fetchRepoTree() function in src/blob.ts makes a single request to GitHub's recursive trees endpoint:

// From src/blob.ts
const url = `https://api.github.com/repos/${ownerRepo}/git/trees/${ref}?recursive=1`;
const response = await fetch(url, { headers });
const tree = await response.json(); // Contains all files/folders with SHAs

Then getSkillFolderHashFromTree() extracts the specific folder's tree SHA:

// From src/blob.ts
export function getSkillFolderHashFromTree(
  tree: GitHubTree,
  skillPath: string
): string | null {
  // Normalize path separators
  const normalizedPath = skillPath.replace(/\\/g, '/');
  // Remove trailing /SKILL.md if present
  const folderPath = normalizedPath.replace(/\/SKILL\.md$/i, '');
  
  const entry = tree.tree.find(
    e => e.type === 'tree' && e.path === folderPath
  );
  return entry?.sha ?? null;
}

Step 3: Compare Hashes and Queue Updates

In src/cli.ts, the updateGlobalSkills() function compares the fetched hash against the stored skillFolderHash:

// From src/cli.ts (updateGlobalSkills)
for (const entry of lockEntries) {
  const latestHash = await fetchSkillFolderHash(
    entry.source,
    entry.skillPath!,
    token,
    entry.ref
  );
  
  if (latestHash !== entry.skillFolderHash) {
    // Hash mismatch = folder changed → needs update
    skillsToUpdate.push(entry);
  }
}

If the hashes differ, the skill is queued for re-installation. The update completes by running skills add <source> -g -y, which writes a fresh skillFolderHash to the lock file.

Practical Example: Manually Checking a skillFolderHash

You can verify the mechanism yourself using the exported functions:

import { fetchSkillFolderHash, readSkillLock } from './skill-lock.ts';

// Check what's stored locally
const lock = await readSkillLock();
const localEntry = lock.find(e => e.source === 'vercel-labs/agent-skills');
console.log('Stored hash:', localEntry?.skillFolderHash);

// Fetch current hash from GitHub
const latestHash = await fetchSkillFolderHash(
  'vercel-labs/agent-skills',
  'skills/react-best-practices',
  process.env.GITHUB_TOKEN
);
console.log('Latest hash:', latestHash);

// Compare
if (latestHash !== localEntry?.skillFolderHash) {
  console.log('Update available!');
}

Summary

  • skillFolderHash is a GitHub tree SHA representing the entire state of a skill's folder
  • The hash is stored in ~/.agents/.skill-lock.json via SkillLockEntry in src/skill-lock.ts
  • Update detection compares the stored hash against a fresh fetch from GitHub's Trees API via fetchRepoTree() in src/blob.ts
  • Hash mismatch triggers re-installation, which writes a new skillFolderHash to the lock file
  • The mechanism is efficient: one API call per skill regardless of how many files are in the folder

Frequently Asked Questions

What happens if skillFolderHash is null or missing?

If getSkillFolderHashFromTree() returns null (folder not found in the tree) or the lock entry lacks skillFolderHash, the CLI treats the skill as needing update. This conservative approach ensures skills don't stay stale if the repository structure changes or the lock file is corrupted.

Does skillFolderHash detect changes to subfolders?

Yes. The GitHub tree SHA recursively includes all nested content. Changing any file at any depth inside the skill folder produces a different tree SHA, which getSkillFolderHashFromTree() will detect when it finds the folder entry in the recursive tree response.

How does the CLI handle rate limits when fetching skillFolderHash?

The fetchRepoTree() function in src/blob.ts accepts an optional token parameter. When provided, requests include Authorization: token <token> headers, raising GitHub's rate limit from 60 to 5,000 requests per hour. Without a token, the CLI may hit limits when checking many skills repeatedly.

Can I manually trigger a skillFolderHash refresh without updating the skill?

Currently, no dedicated command exists. The hash is only written during skills add (installation) or skills update (detected change). To force a refresh without changes, you could manually edit ~/.agents/.skill-lock.json to remove the skillFolderHash field, prompting the next check to treat it as stale and re-fetch.

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 →