Skip to content

fix(cli): keep project folders from choosing the programs and config the CLI runs - #7511

Merged
michaelwang11394 merged 11 commits into
mainfrom
harden-cli-project-exec
Nov 10, 2026
Merged

michaelwang11394 merged 11 commits into
mainfrom
harden-cli-project-exec

Conversation

@michaelwang11394

@michaelwang11394 michaelwang11394 commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

What

Keep a project folder from choosing the programs, imports and configuration the CLI and skill scripts run:

  • Resolve helper programs from PATH, never the working directory. On Windows every CLI program entry (the CLI, the warm-check client and server, the workers and the font tool) sets NoDefaultCurrentDirectoryInExePath before it spawns anything, and child processes inherit it. The documented .hyperframes/bin and @ffmpeg-installer ffmpeg lookups are kept.
  • Python helpers started with -c/-m no longer import modules from the working directory.
  • The skills installer runs from the home folder (the temp folder when HOME does not exist), so a project's node_modules or .npmrc cannot substitute the pinned package. media-use helper scripts started by hyperframes media-use run nested npx hyperframes calls (including the Kokoro voice fallback) through the CLI that started them. Scripts started directly still use npx as before.
  • whisper.cpp source builds use a pinned release (v1.9.5), checked by commit, instead of the default branch.
  • media-use audio scripts accept only provider keys and harmless settings from a project .env (the same policy as the CLI). Credential paths, HEYGEN_API_BASE, HYPERFRAMES_PYTHON, HYPERFRAMES_CONFIG_DIR and similar still work from the shell.
  • Registry installs refuse targets inside .git, hidden tool directories, hidden config files, or package.json/hyperframes.json (except an example's own files on init). Windows short-name segments such as CLAUDE~1 or PACKAG~1.JSO are refused too. Trailing dots, spaces and stream names were already refused when the file is written. Manifest caches are keyed by a digest of the registry URL and record their source, so two registries never share a cache entry.
  • Capture keeps page text to one line in generated agent notes, keeps an existing CLAUDE.md, AGENTS.md or .cursorrules it did not write (with a warning), and accepts only a plain service-account key in HYPERFRAMES_VERTEX_SERVICE_ACCOUNT.

Why

Opening or working in a project from someone else should not let that project run code or redirect credentials through the CLI.

Related work

None.

How

Local changes in the process-spawn helpers, the Python runner, the skills/registry commands, the whisper manager, capture, and the media-use env loader. Local Studio mode in preview (running the project's own @hyperframes/studio when installed) is documented behaviour and is unchanged.

Test plan

  • Unit tests added/updated: 344 CLI vitest, 21 parsers vitest and 52 node --test pass.
  • fails without the fix: starts the next Windows warm-check server with the working-directory exe lookup off, each one that starts its own process turns the lookup off before it spawns anything, and refuses the configuration target for CLAUDE~1/settings.json, VSCODE~1/tasks.json, PACKAG~1.JSO and NPMRC~1, shown by running them with the fix stashed (2 and 4 failures).
  • Lefthook hooks, fallow, oxlint and the comment ratchet pass. CLI typecheck reports only errors from the unbuilt inspector package, none in changed files.
  • Follow-up: the CLI test-home check now finds the digest-named registry cache, and the new Windows helper carries no comment (comment ratchet).
  • Not run on Windows.
  • Behaviour notes: a Windows user who kept a tool such as ffmpeg.exe loose in the project folder must put it on PATH or in .hyperframes/bin. Third-party registry items that install dotfiles or package.json are refused; no item in this repo's registry is affected (checked all 457). Each registry cache refetches once after upgrading.

🤖 Generated with Claude Code

michaelwang11394 and others added 7 commits November 10, 2026 10:06
On Windows a bare command name is looked up in the working directory before
PATH. The CLI now sets NoDefaultCurrentDirectoryInExePath at startup, which
Node's spawn and cmd.exe both honor and child processes inherit, so tools
such as powershell.exe, taskkill and npx.cmd always come from PATH.

The shared ffmpeg/ffprobe resolver no longer scans the working directory on
Windows and skips relative PATH entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ectory

`python -c` and `python -m` put the working directory first on sys.path, so
a file in the project named like a probed module ran instead of the
installed package. Inline helper code now drops that entry before any
import, and `-m` helpers (pip) run through runpy after the same step. Paths
and arguments are unchanged.

On Windows, findPython also skips a `where` match in the working directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f the project

The global skills install ran `npx skills@<pin>` in the current folder, so
npm could take the package or registry from that project's node_modules or
.npmrc. It now runs from the home folder; the install is global either way.

When the CLI starts the media-use engine it hands over its own entry file.
The local voice fallback and the audio helpers then run that same CLI with
the same Node instead of resolving `npx hyperframes` from the project.
Without a handed-over entry they behave as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The audio scripts copied every key of a project .env into their environment
except two. They now take the same kind of short allowlist the CLI uses:
HeyGen, ElevenLabs and Gemini API keys plus a few harmless settings. Program
overrides, runtime options, credential files and the credential store path
come only from the person's own environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The source-build fallback cloned the upstream default branch. It now clones
the v1.9.5 release tag and refuses to build unless the checkout is the
reviewed commit for that tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Registry cache file names now include a digest of the exact registry URL,
and each entry records the URL it came from, so one registry's cached
manifest is never read for another. Entries from older versions are
refetched once.

Registry installs refuse targets inside .git or any hidden directory, hidden
configuration files, and package.json, hyperframes.json and
hyperframes.lock.json. An example installed by `init` may still bring its
own package.json and .gitignore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Vertex keys

Capture writes AGENTS.md, CLAUDE.md and .cursorrules from the page's title,
font names and colours. Page text is now flattened to one short line and
colours must be hex values. Existing agent files that capture did not write
are kept, with a warning.

HYPERFRAMES_VERTEX_SERVICE_ACCOUNT must now be a plain service-account key
with a Google token endpoint; other credential types skip captioning with a
warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 94 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

Unstable (1)

  • seqab-none-center-r0-root-z100: tracking 0.03, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.03, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true / tracking 0.03, pressJump 0, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport true

michaelwang11394 and others added 2 commits November 10, 2026 10:21
… comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l without HOME

Windows drops trailing dots and spaces from a path segment and reads name:stream as
name, so package.json. or .git./hooks reached the files the target rules refuse.
The global skills install now falls back to the temp folder when HOME does not exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 1 blocker · 1 important · 3 nits · head 039ebac

  • blocker packages/cli/src/warmCheck.ts:1-6: the warm-check server is a separate tsup entry, and it never imports ./utils/windowsExeSearch.js. bin/hyperframes.mjs loads warmCheckClient before cli.js, and launchEnv = { ...process.env } is copied before the variable is set. startNextWarmCheck spawns the server with that env and cwd = project. The server's startup prelaunchBrowserForCheck → watchChromeTree → windowsChromeWatchdog.ts:8 then runs a bare powershell.exe without NoDefaultCurrentDirectoryInExePath. A non-TTY hyperframes check on Windows in a project that contains powershell.exe therefore runs the project's copy (traced through libuv search_path; not run on Windows). Fix: import windowsExeSearch first in warmCheck.ts, and set the variable in launchEnv too.
  • important packages/cli/src/registry/installer.ts:146,160: only the 8.3 short name of .git is refused. A registry item can target CLAUDE~1/settings.json, VSCODE~1/tasks.json, PACKAG~1.JSO or NPMRC~1, and publication.ts realpaths existing segments, so on NTFS with 8.3 names these write the protected files. A probe confirmed all four pass the check. Fix: refuse /~\d+(\.|$)/ segments, or apply the rules to the realpath'd relative path.
  • nit installer.ts:171-176: on base, unsafeSegment already rejects the Windows-name cases (package.json., ::$DATA, .git./…) and hyperframes.lock.json, so those 5 test cases pass on base. That contradicts "each new test fails without its fix".
  • nit installer.ts:134-140: the new constants sit between assertSafeTarget's JSDoc and the function.
  • nit whisper/manager.ts:191: assertPinnedCheckout runs only on a fresh clone, so an existing cache checkout from an older CLI is built unchecked.
Scope

I read the full non-test diff and warmCheck/warmCheckClient/bin. These hold: the whisper pin (tag v1.9.5 = d1be6fde), the Python -m isolation (a planted json.py is not imported, pip still runs), and the media-use .env allowlist. Local Studio vite and .hyperframes/bin are kept. CLI vitest 161/162 pass (testHome needs a bun binary), and parsers and node --test suites pass. CI is green.

— Genesis

michaelwang11394 and others added 2 commits November 10, 2026 11:37
…ram entry

The warm-check server and client are their own tsup entries and copied the
environment before cli.js set NoDefaultCurrentDirectoryInExePath, so the server
spawned a bare powershell.exe that Windows could take from the project folder.
Every entry that hides child consoles now also loads windowsExeSearch first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A segment like CLAUDE~1 or PACKAG~1.JSO opens the long name on NTFS, so it
reached the files the target rules refuse. The trailing-dot and stream-name
normalization is removed: publication already rejects those segments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michaelwang11394

Copy link
Copy Markdown
Contributor Author

Review follow-up:

  • blocker (warm-check server): fixed in d1512e8. warmCheck.ts and warmCheckClient.ts import windowsExeSearch first, so launchEnv and the server both carry the variable. The CLI's other own entries (workers, hang report, font tool) do too. New tests: the client test fails without the fix, and so does the guard that every program entry loads it.
  • important (8.3 short names): fixed in 9a7d15b. Any segment matching ~<digits> followed by a dot or the end is refused. CLAUDE~1/, VSCODE~1/, PACKAG~1.JSO and NPMRC~1 are covered and fail without the fix. No item in this repo's registry uses one.
  • nit (cases that pass on base): fixed in 9a7d15b. I removed the redundant trailing-dot and stream-name normalization and its four cases, because publication already refuses them. The PR body now names only the tests that fail without the fix.
  • nit (JSDoc placement): fixed in 9a7d15b.
  • nit (whisper existing checkout): not changed. buildFromSource runs only when no built binary exists, and a checkout without a binary is already deleted and cloned again at the pinned tag. An older binary that is already built is kept on purpose, so users are not forced to rebuild.

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 3 resolved, 0 open, 0 new · 039ebac → 9a7d15b. warmCheck.ts/warmCheckClient.ts (and the other process entries) import windowsExeSearch first. The spawned server env carries the setting, and a tsup-entry census pins it. Short-name segments are refused. With the previous sources, the census, warm-check spawn and all 4 short-name tests go red; at head, 38/38 pass. CI is green.

— Genesis

@michaelwang11394
michaelwang11394 added this pull request to the merge queue Nov 10, 2026
Merged via the queue into main with commit 84c7d02 Nov 10, 2026
131 of 132 checks passed
@michaelwang11394
michaelwang11394 deleted the harden-cli-project-exec branch November 10, 2026 20:37
@miguel-heygen miguel-heygen mentioned this pull request Nov 10, 2026
3 of 4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant