From 4499b860be5cbf551b2a0b3367ec433181556c21 Mon Sep 17 00:00:00 2001 From: artale Date: Mon, 15 Jun 2026 04:35:47 +0200 Subject: [PATCH] fix: strip hidden unicode from imported context --- src/core/unicode-safety.test.ts | 12 ++++++++++++ src/core/unicode-safety.ts | 11 +++++++++++ src/pai/pai-connector.ts | 11 ++++++----- src/pai/skill-sync.ts | 5 +++-- 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 src/core/unicode-safety.test.ts create mode 100644 src/core/unicode-safety.ts diff --git a/src/core/unicode-safety.test.ts b/src/core/unicode-safety.test.ts new file mode 100644 index 0000000..d2b3a7e --- /dev/null +++ b/src/core/unicode-safety.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { hasHiddenUnicode, stripHiddenUnicode } from "./unicode-safety.js"; + +describe("unicode safety", () => { + it("strips hidden instruction carriers", () => { + const tag = String.fromCodePoint(0xe0069); + const text = `safe${tag}\u200b\u202etext`; + + expect(hasHiddenUnicode(text)).toBe(true); + expect(stripHiddenUnicode(text)).toBe("safetext"); + }); +}); diff --git a/src/core/unicode-safety.ts b/src/core/unicode-safety.ts new file mode 100644 index 0000000..bb5299d --- /dev/null +++ b/src/core/unicode-safety.ts @@ -0,0 +1,11 @@ +const HIDDEN_UNICODE = /[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFE00-\uFE0F\u{E0000}-\u{E007F}]/gu; + +export function stripHiddenUnicode(text: string): string { + // ponytail: strips known invisible instruction carriers; add reporting metadata if UI needs forensics. + return text.replace(HIDDEN_UNICODE, ""); +} + +export function hasHiddenUnicode(text: string): boolean { + HIDDEN_UNICODE.lastIndex = 0; + return HIDDEN_UNICODE.test(text); +} diff --git a/src/pai/pai-connector.ts b/src/pai/pai-connector.ts index cbb5d7b..2bd5666 100644 --- a/src/pai/pai-connector.ts +++ b/src/pai/pai-connector.ts @@ -1,6 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; +import { stripHiddenUnicode } from "../core/unicode-safety.js"; const PAI_ROOT = path.join(os.homedir(), ".claude", "PAI"); @@ -55,7 +56,7 @@ export class PaiConnector { return { path: full, name: f.replace(/\.md$/, ""), - content: fs.readFileSync(full, "utf-8"), + content: stripHiddenUnicode(fs.readFileSync(full, "utf-8")), }; }); } @@ -70,7 +71,7 @@ export class PaiConnector { return fs.statSync(skillPath).isDirectory() && fs.existsSync(path.join(skillPath, "SKILL.md")); }).map((f) => { const skillPath = path.join(legacyDir, f); - const skillMd = fs.readFileSync(path.join(skillPath, "SKILL.md"), "utf-8"); + const skillMd = stripHiddenUnicode(fs.readFileSync(path.join(skillPath, "SKILL.md"), "utf-8")); const desc = skillMd.split("\n").slice(0, 5).join(" ").slice(0, 200); return { name: f, path: skillPath, description: desc }; }); @@ -81,7 +82,7 @@ export class PaiConnector { return fs.statSync(skillPath).isDirectory() && fs.existsSync(path.join(skillPath, "SKILL.md")); }).map((f) => { const skillPath = path.join(skillsDir, f); - const skillMd = fs.readFileSync(path.join(skillPath, "SKILL.md"), "utf-8"); + const skillMd = stripHiddenUnicode(fs.readFileSync(path.join(skillPath, "SKILL.md"), "utf-8")); const desc = skillMd.split("\n").slice(0, 5).join(" ").slice(0, 200); return { name: f, path: skillPath, description: desc }; }); @@ -107,7 +108,7 @@ export class PaiConnector { if (isaFiles.length === 0) return null; const latest = isaFiles.sort((a, b) => b.name.localeCompare(a.name))[0]; - const content = fs.readFileSync(latest.path, "utf-8"); + const content = stripHiddenUnicode(fs.readFileSync(latest.path, "utf-8")); const sections = this.parseSections(content); return { path: latest.path, name: latest.name, content, sections }; @@ -116,7 +117,7 @@ export class PaiConnector { readFile(relativePath: string): string | null { const full = path.join(this.paiRoot, relativePath); if (!fs.existsSync(full)) return null; - return fs.readFileSync(full, "utf-8"); + return stripHiddenUnicode(fs.readFileSync(full, "utf-8")); } private parseSections(content: string): Map { diff --git a/src/pai/skill-sync.ts b/src/pai/skill-sync.ts index 1992693..405e6d3 100644 --- a/src/pai/skill-sync.ts +++ b/src/pai/skill-sync.ts @@ -3,6 +3,7 @@ import * as path from "node:path"; import * as os from "node:os"; import type { SkillDefinition, SkillStep } from "../core/types.js"; import { StateStore } from "../core/state-store.js"; +import { stripHiddenUnicode } from "../core/unicode-safety.js"; const PAI_SKILLS_DIR = path.join(os.homedir(), ".claude", "skills"); @@ -31,7 +32,7 @@ export class SkillSync { const skillMd = path.join(this.paiSkillsDir, entry.name, "SKILL.md"); if (!fs.existsSync(skillMd)) continue; - const content = fs.readFileSync(skillMd, "utf-8"); + const content = stripHiddenUnicode(fs.readFileSync(skillMd, "utf-8")); const description = this.extractDescription(content); const steps = this.extractSteps(content); const tags = this.extractTags(content); @@ -118,7 +119,7 @@ export class SkillSync { const skillPath = path.join(this.paiSkillsDir, skillName, "SKILL.md"); if (!fs.existsSync(skillPath)) return null; - let content = fs.readFileSync(skillPath, "utf-8"); + let content = stripHiddenUnicode(fs.readFileSync(skillPath, "utf-8")); // Find or create a Lessons Learned section const lessonsHeader = "## Lessons Learned";