Skip to content

feat(plugins): let a host command run only verified bytes - #2535

Merged
shellz-n-stuff merged 2 commits into
mainfrom
claude/host-command-artifacts
Nov 5, 2026
Merged

shellz-n-stuff merged 2 commits into
mainfrom
claude/host-command-artifacts

Conversation

@shellz-n-stuff

Copy link
Copy Markdown
Collaborator

Fixes the "Hash-checked host commands" follow-up from #2334 (#1875 finding A).

Plugin host commands always ran the first matching program on PATH, and the host refuses absolute program paths. So the Buzz Security companion's status, review and checker commands run env from the user's PATH. Among other things, those commands record approval decisions. Anything that can put an env earlier on PATH, such as ~/.local/bin (first on macOS), would run in their place.

Changes

  • Manifest (crates/plugin-manager/src/lib.rs). host.commands entries may declare the same optional artifact: {path, sha256, siblings?} that host.processes already can. It is checked by the same valid_artifact. Manifests without it parse and serialize unchanged.
  • Host (src-tauri/src/host_command.rs, src-tauri/src/host_process.rs).
    • A command that declares an artifact runs only after its installed bytes and siblings verify in place. This uses the same verify_artifact as uncontained processes, after the same ~/ expansion.
    • Verification runs off the async runtime and inside the existing 5 s deadline.
    • If verification fails, the command returns null, with no PATH fallback.
  • Plugin contract (src/plugins/api.ts, FOUNDATION). One optional artifact field on commands, matching the process descriptor. The owner approved this.
  • Docs.
    • docs/agent-protection.md says commands may declare an artifact and are verified in place, not snapshotted.
    • docs/plugin-authoring-guide.md spells out the descriptor. It also notes that interpreters and programs the command calls by name are still found on PATH, so use an absolute shebang and absolute paths.

Release order

  • Older Buzz builds reject a manifest whose command declares artifact (deny_unknown_fields), and fail closed with an install error.
  • The companion's switch to artifact-checked commands must therefore ship after the Buzz release that carries this. That ordering is already tracked in squareup/buzz-plugin-security#4.

Testing

  • cargo test validates_host_declarations in crates/plugin-manager passes. A valid and a ~/ descriptor are accepted; a relative path and an uppercase hash are refused.
  • cargo test --lib host_command:: in src-tauri passes: 15 tests.
    • The new artifact_commands_run_only_verified_bytes runs a pinned script.
    • Its args would print "fallback" through a PATH env, so the test also proves that a tampered script returns null instead of falling back.
    • Both tests fail without the change, because the old manifest rejects the key.
  • pnpm typecheck passes.
  • Under heavy local load, some existing timing-sensitive host_command tests failed once. They passed on two unloaded reruns. This change doesn't touch them.

Review checklist

  • Agent review: an independent reviewer found no blockers (9/10). Its test and authoring-guide suggestions were applied.
  • Changed behavior exercised in the app: deferred. It needs one tauri dev run with the updated companion installed from folder. The status command should load, and approving or denying a pending request should record the decision.
  • Human testing: pending.

🤖 Generated with Claude Code

shellz-n-stuff and others added 2 commits November 4, 2026 17:31
A host command always ran the first matching program on PATH, so a plugin
could not pin one to the executable it installed. Commands may now declare
the same `artifact` descriptor as processes; each run verifies the installed
bytes in place and never looks the program up on PATH.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Alex Rosenzweig <[email protected]>
Also spell out the command artifact descriptor and its PATH caveat for
plugin authors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Alex Rosenzweig <[email protected]>
@shellz-n-stuff
shellz-n-stuff requested a review from a team as a code owner November 4, 2026 23:45
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T23:47:48.922199Z 017b46d PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 017b46d56d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +412 to +415
tokio::task::spawn_blocking(move || crate::host_process::verified_artifact(artifact))
.await
.ok()?
.ok()?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Execute the artifact bytes that were actually verified

When another process can write the artifact directory—the threat this hash pinning is intended to mitigate—verified_artifact closes the verified files and returns only their path, and this code subsequently reopens that path during Command::spawn. An attacker can atomically replace the executable or a declared sibling between verification and spawning, causing unchecked bytes to run and potentially forge approval decisions. Launch from an immutable verified snapshot or otherwise bind execution to the same opened file rather than resolving the path again.

Useful? React with 👍 / 👎.

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.

[P1] Bind command execution to the verified bytes

I agree this remains an integrity gap: verification returns a pathname, while execution subsequently opens that pathname again. The new regression test checks modification between separate calls, not whether the verified bytes remain the bytes executed. Please use a mutation-protected verified generation or an equivalent guarantee covering the executable and declared siblings.

Posted by Codex (gpt-6.1-sol) on behalf of kylez

@joahg joahg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Closes the PATH-hijack gap for host commands, with fail-closed behavior and tests for both the tampered and the valid path. Approving with the TOCTOU caveat in the inline comment.

tokio::task::spawn_blocking(move || crate::host_process::verified_artifact(artifact))
.await
.ok()?
.ok()?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 P2 (not blocking): verify_artifact returns a path that spawn reopens, so a writer to the artifact directory can swap the file between verification and exec. This matches the existing uncontained-process behavior and the docs say verification is in place, and it fixes the PATH hijack the PR targets. A follow-up that execs a verified snapshot (or the same opened fd) would close the race Codex and Kyle flagged.

@shellz-n-stuff
shellz-n-stuff merged commit adf49b5 into main Nov 5, 2026
40 checks passed
@shellz-n-stuff
shellz-n-stuff deleted the claude/host-command-artifacts branch November 5, 2026 20:05
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…-service

* origin/main: (29 commits)
  Normalize profile names for verified-name matching (#2348)
  Add BuzzTerm font settings and compact glyph spacing (#2502)
  feat(plugins): let a host command run only verified bytes (#2535)
  NEXUS-4228: Add plugin toolbar actions and panel links (#2645)
  Keep plugin update controls stable and spaced (#2636)
  feat(inbox): rename to Activity and simplify the filter bar (#2543)
  fix(goose): track every replaced protected worker and check its security gate (#2528)
  docs(agents): say Pi agents don't take plugin notes yet (#2534)
  feat(agents): reorder agent cards with hold and drag (#1837)
  fix(messages): keep own sends from before the loaded history off the timeline (#2525)
  feat(github): rework the GitHub settings page (#2450)
  fix(mcp): accept create scopes in durable approvals (#2650)
  feat(bestie): Bestie tab and Remembers you for a hosted Bestie (#2478)
  fix(relay): p-tag a one-to-one DM's agent partner on new messages (#2475)
  fix(agents): show each message author's username and display name (#2628)
  fix(agents): preserve visible edits in conversation history (#2355)
  fix(canvas): keep todo edits from approving unrelated instructions (#2350)
  fix(messages): keep messages in their canonical channel (#2349)
  NEXUS-4165: Add Thinking to agent creation (#2409)
  feat(plugins): let a plugin paint a backdrop behind the shell (#2437)
  ...

# Conflicts:
#	src/bundled/goose/runtime.ts
wpfleger96 added a commit that referenced this pull request Nov 5, 2026
…leanup

* origin/main:
  Normalize profile names for verified-name matching (#2348)
  Add BuzzTerm font settings and compact glyph spacing (#2502)
  feat(plugins): let a host command run only verified bytes (#2535)
  NEXUS-4228: Add plugin toolbar actions and panel links (#2645)
  Keep plugin update controls stable and spaced (#2636)
  feat(inbox): rename to Activity and simplify the filter bar (#2543)

# Conflicts:
#	src/bundled/inbox/InboxPage.test.tsx
#	tests/browser/inbox-archive.spec.mjs
#	tests/fixtures/profiles.tsx
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.

3 participants