From 8c8cdfcb86ca7cbecea1fbe4509f59f528267eba Mon Sep 17 00:00:00 2001 From: Osraka <98612432+Osraka@users.noreply.github.com> Date: Thu, 8 Oct 2026 23:28:22 +0300 Subject: [PATCH] fix(update): frame project skill hash inputs --- src/local-lock.ts | 7 +++++-- tests/local-lock.test.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/local-lock.ts b/src/local-lock.ts index c5d1cd950..6ce5b0959 100644 --- a/src/local-lock.ts +++ b/src/local-lock.ts @@ -149,7 +149,7 @@ function getPortableLocalSource(source: string, lockDir: string): string { /** * Compute a SHA-256 hash from all files in a skill directory. * Reads all files recursively, sorts them by relative path for determinism, - * and produces a single hash from their concatenated contents. + * and produces a single hash from their framed paths and contents. */ export async function computeSkillFolderHash(skillDir: string): Promise { const files: Array<{ relativePath: string; content: Buffer }> = []; @@ -160,8 +160,11 @@ export async function computeSkillFolderHash(skillDir: string): Promise const hash = createHash('sha256'); for (const file of files) { - // Include the path in the hash so renames are detected + // Frame each file so a path change cannot be offset by a content change. hash.update(file.relativePath); + hash.update('\0'); + hash.update(String(file.content.length)); + hash.update('\0'); hash.update(file.content); } diff --git a/tests/local-lock.test.ts b/tests/local-lock.test.ts index a066b04e0..0465b8e17 100644 --- a/tests/local-lock.test.ts +++ b/tests/local-lock.test.ts @@ -455,6 +455,24 @@ describe('local-lock', () => { } }); + it('distinguishes a filename change from an offsetting content change', async () => { + const dir = await mkdtemp(join(tmpdir(), 'lock-test-')); + try { + const first = join(dir, 'first'); + const second = join(dir, 'second'); + await mkdir(first); + await mkdir(second); + await writeFile(join(first, 'SKILL.md'), 'same skill'); + await writeFile(join(second, 'SKILL.md'), 'same skill'); + await writeFile(join(first, 'a'), 'bc'); + await writeFile(join(second, 'ab'), 'c'); + + expect(await computeSkillFolderHash(first)).not.toBe(await computeSkillFolderHash(second)); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + it('includes nested files in subdirectories', async () => { const dir = await mkdtemp(join(tmpdir(), 'lock-test-')); try {