From 8c31b8514afb751d3d105df1b3fe30c0aec9c316 Mon Sep 17 00:00:00 2001 From: Bruno Azoulay Date: Wed, 14 Oct 2026 12:15:27 +0200 Subject: [PATCH 1/2] fix(motion): stop key-word heuristic from blocking unrelated commands The Motion signing key guard fired in every session with fuse-motion enabled on any command containing the word "key" next to a glob or variable character (python lambdas, SQL columns, ssh scripts). That branch protected nothing a simple glob variant did not already bypass, so it is removed; the glob/variable + .fuse-har heuristic and every path-based protection of the motion signing key directory are unchanged. Tests: test/motion-key-word-rule.test.ts (4 former false positives now allowed, 5 groups of real key-directory accesses still blocked). --- MEMORY/LESSON.md | 3 ++ src/policy/motion/guard-key.ts | 2 +- test/motion-key-word-rule.test.ts | 53 +++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 test/motion-key-word-rule.test.ts diff --git a/MEMORY/LESSON.md b/MEMORY/LESSON.md index 1f1b1df..387db99 100644 --- a/MEMORY/LESSON.md +++ b/MEMORY/LESSON.md @@ -89,3 +89,6 @@ - [2026-09-05 13:14] Post-patch line count by hunk arithmetic (`Σ new−old`) exact on 4000 random cases, off by one at edge (`countLines` swallows FILE's final `\n`, not hunk's) → predict file size after patch: APPLY patch to real text, count once with same function as disk count; never sum segment deltas. [TRIGGERS path:src/policy/edit-outcome.ts,src/adapters/codex/apply-patch.ts keyword:countLines,delta,hunk,post-patch] - [2026-10-04 22:48] Child-track merge (Cursor): `recordDoc` cross-credited `target`, `max(parentAt, childAt)` inherited ANOTHER session's stamp, stale read became fresh → to merge proof credit only present framework via `creditDocConsultation`; take max only if target session itself earned stamp (`sessions` AND `doc_sessions`). [TRIGGERS path:src/freshness/child-evidence-merge.ts keyword:merge,recordDoc,target,doc_consulted] + +- [2026-10-14 00:00] (×4) Motion key guard false-positive fix: rewrite + 3 "proven-inert" exemption rounds each REFUTED by challenger (in-word quotes, GLOBIGNORE + `~+/..`, failed `cd nope;` cwd drift, OLDPWD var, deep glob through symlink, `rg -L`) → guard FP fix = new = old AND NOT exempt by construction (old-ALLOW→new-BLOCK = 0 automatic); exemption = allowlist of exact owner FP shapes, never general shell-path modelling; every flip shell-executed bash+zsh vs planted secret before verdict; challenger after EACH round, never trust executor "reach=0". [TRIGGERS path:src/policy/motion/guard-key.ts,src/policy/motion/key-exempt.ts keyword:guard,exempt,false positive,faux positif,glob] +- [2026-10-14 10:51] 6 rounds building exemption allowlist for blunt motion guard word-heuristic (word + glob/var anywhere in cmd); real usage (cd prefix, python lambda, SQL column) still blocked 16/16; owner furious: plugin-scoped guard firing in unrelated sessions → first ask "does heuristic protect anything real?" (trivial variant like k*s glob already passed) → if not, remove heuristic, keep only path-based blocks; never pile exemptions on worthless rule. diff --git a/src/policy/motion/guard-key.ts b/src/policy/motion/guard-key.ts index aa6b1b4..c9d50ee 100644 --- a/src/policy/motion/guard-key.ts +++ b/src/policy/motion/guard-key.ts @@ -44,7 +44,7 @@ export function motionKeyViolation(event: NormalizedEvent, cwd: string, home: st if (!name) return false; if (name.includes(MOTION_KEY_FRAGMENT) || /\.fuse-harness\/motion[*?\[]/.test(name)) return true; if (/\.fuse-harness\/(?:[*?\[]|$)/.test(name)) return true; - if (/[$*?[]/.test(name) && /(?:\.fuse-har|\bkeys?\b)/.test(name)) return true; + if (/[$*?[]/.test(name) && /\.fuse-har/.test(name)) return true; const path = canonicalFilePath(homePath(name, cwd, home)); return isUnder(path, directory) || ((/grep|glob|search/i.test(event.tool) || MOTION_WRITE_TOOLS.has(event.tool)) && isUnder(directory, path)); }); diff --git a/test/motion-key-word-rule.test.ts b/test/motion-key-word-rule.test.ts new file mode 100644 index 0000000..849b4b4 --- /dev/null +++ b/test/motion-key-word-rule.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, realpathSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { normalizeEvent } from "../src/runtime/normalize"; +import { motionKeyViolation } from "../src/policy/motion/guard-key"; +import { motionKeyDirectory } from "../src/policy/motion/store"; + +const home = realpathSync(mkdtempSync(join(tmpdir(), "motion-keyword-"))); +const keyDir = motionKeyDirectory(home); +mkdirSync(keyDir, { recursive: true }); +const cwd = join(home, "work"); +mkdirSync(cwd, { recursive: true }); + +/** Run the key guard for one tool call and return the verdict kind (null = allowed). */ +const verdict = (tool: string, input: Record): string | null => + motionKeyViolation(normalizeEvent("claude-code", { tool_name: tool, tool_input: input }), cwd, home)?.kind ?? null; +const bash = (command: string): string | null => verdict("Bash", { command }); + +test("ALLOW 1 ssh + python with lambda key= sort and slicing", () => { + expect(bash(`ssh -o BatchMode=yes root@10.10.11.9 "python3 -c \\"import sqlite3,re; R=[r for r in rows]; print(sorted(kinds.items(),key=lambda x:-x[1])[:12])\\""`)).toBeNull(); +}); +test("ALLOW 2 python sort(key=lambda) with slicing", () => { + expect(bash(`cd /tmp/x && python3 -c "out.sort(key=lambda r: r[0]); print(out[:5])"`)).toBeNull(); +}); +test("ALLOW 3 sqlite3 query selecting a key column", () => { + expect(bash(`cd /tmp/x && sqlite3 -readonly g/state.vscdb "select count(*), substr(key,1,12) from cursorDiskKV where key like 'bubbleId:%'"`)).toBeNull(); +}); +test("ALLOW 4 python json keys() slicing", () => { + expect(bash(`python3 -c "import json; d=json.load(open('a.json')); print(sorted(d.keys())[:60])"`)).toBeNull(); +}); + +test("BLOCK 5 literal key file paths", () => { + expect(bash("cat ~/.fuse-harness/motion-keys/x.key")).toBe("block"); + expect(bash(`cat ${join(keyDir, "x.key")}`)).toBe("block"); +}); +test("BLOCK 6 glob and variable paths to the harness directory", () => { + for (const command of ["cat ~/.fuse-har*/motion*/*", "cat $HOME/.fuse-harness/*", "cat ~/.fuse-harness/m*/*", "ls ~/.fuse-harness/"]) { + expect(bash(command)).toBe("block"); + } +}); +test("BLOCK 7 variable indirection into the harness directory", () => { + expect(bash("D=$HOME/.fuse-harness; cat $D/m*/*")).toBe("block"); +}); +test("BLOCK 8 Read, Grep and Glob tools", () => { + expect(verdict("Read", { file_path: join(keyDir, "x.key") })).toBe("block"); + expect(verdict("Grep", { path: keyDir, pattern: "." })).toBe("block"); + expect(verdict("Glob", { pattern: join(home, ".fuse-harness/*") })).toBe("block"); +}); +test("BLOCK 9 recursive grep and rg over the harness directory", () => { + expect(bash("grep -r foo ~/.fuse-harness")).toBe("block"); + expect(bash("rg foo ~/.fuse-harness/motion-keys")).toBe("block"); +}); From f0b1227224d6dedf497c1b0c9c21860c5f5bce7e Mon Sep 17 00:00:00 2001 From: Bruno Azoulay Date: Wed, 14 Oct 2026 12:16:07 +0200 Subject: [PATCH 2/2] chore: bump version and CHANGELOG to 0.1.109 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70a255..68c2059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to `@fusengine/harness`. Format: [Keep a Changelog](https:// ## [Unreleased] +## [0.1.109] - 2026-10-14 + +### Fixed + +- **motion: key-word heuristic no longer blocks unrelated commands** (`src/policy/motion/guard-key.ts`) — the Motion signing key guard fired in every session with fuse-motion enabled on any command containing the word "key" next to `$ * ? [` (python lambdas, SQL columns, ssh scripts). That branch protected nothing a simple glob variant did not already bypass and is removed; the glob/variable + `.fuse-har` heuristic and every path-based protection of the signing key directory are unchanged. Tests: `motion-key-word-rule`. + ## [0.1.108] - 2026-10-12 ### Changed diff --git a/package.json b/package.json index ed5b8f4..588ee84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@fusengine/harness", - "version": "0.1.108", + "version": "0.1.109", "description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.", "type": "module", "module": "src/index.ts",