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

> Discover how the skillFolderHash mechanism in Vercel Skills uses GitHub tree SHAs to quickly detect skill updates, bypassing slow per-file checks and streamlining your workflow.

- Repository: [Vercel Labs/skills](https://github.com/vercel-labs/skills)
- Tags: internals
- Published: 2026-04-23

---

**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`](https://github.com/vercel-labs/skills/blob/main/src/skill-lock.ts):

```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`](https://github.com/vercel-labs/skills/blob/main/src/skill-lock.ts) loads the global lock file and returns all installed skill entries with their cached `skillFolderHash` values.

```ts
// 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`](https://github.com/vercel-labs/skills/blob/main/src/skill-lock.ts) retrieves the current folder hash from GitHub. It delegates to [`src/blob.ts`](https://github.com/vercel-labs/skills/blob/main/src/blob.ts) for the actual API interaction:

```ts
// 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`](https://github.com/vercel-labs/skills/blob/main/src/blob.ts) makes a single request to GitHub's recursive trees endpoint:

```ts
// 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:

```ts
// 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`](https://github.com/vercel-labs/skills/blob/main/src/cli.ts), the `updateGlobalSkills()` function compares the fetched hash against the stored `skillFolderHash`:

```ts
// 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:

```ts
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`](https://github.com/vercel-labs/skills/blob/main/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`](https://github.com/vercel-labs/skills/blob/main/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`](https://github.com/vercel-labs/skills/blob/main/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.