From 5ca7b4c1fefbf9afd5d4e1018c3ac728fc29a0ac Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Thu, 13 Aug 2026 16:43:21 -0700 Subject: [PATCH] fix(security): standardize paperclipai CLI guidance on safe npx path (#11343) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip provides CLI guidance to agents and operators through documentation and runtime messages. > - Content-bearing `pnpm paperclipai` examples send arguments through a shell. > - Shell evaluation can execute command substitutions in untrusted argument content. > - Runtime hostname guidance can also place request-derived content inside a shell command. > - This pull request uses `npx paperclipai` for content-bearing guidance and uses a static hostname placeholder. > - The benefit is safer copy-paste guidance for agents and operators. ## Linked Issues or Issue Description **Issue type** Incorrect information **Where is the issue?** CLI guidance in `doc/CLI.md`, `skills/paperclip/SKILL.md`, documentation, and runtime-generated hints. **What's wrong?** Content-bearing `pnpm paperclipai` commands can pass argument text through `/bin/sh`. Shell command substitution in an argument can execute before the CLI receives the value. **Suggested fix** Use `npx paperclipai` for content-bearing commands. Use a static `` placeholder when runtime guidance displays the allowed-hostname command. ## What Changed - Replace content-bearing `pnpm paperclipai` examples with `npx paperclipai` across the documentation and agent-facing guidance. - Update runtime-generated CLI hints to use a static `` placeholder. - Add safety notes to `doc/CLI.md` and `skills/paperclip/SKILL.md`. - Add scans and regression tests for unsafe invocation and hostile hostname headers. - Keep fixed lifecycle commands and `pnpm --filter @paperclipai/*` build commands unchanged. ## Verification - Run `tsc --noEmit` for the changed server files. - Run `cli-invocation-safety.test.ts`. - Run `private-hostname-guard.test.ts`. - Confirm that hostile hostname headers do not enter shown shell commands. - Confirm that the three commits contain the required Paperclip co-author trailer. ## Risks - This change updates documentation and diagnostic text across many surfaces. - Fixed lifecycle and setup commands remain unchanged. - The tests fail if content-bearing `pnpm paperclipai` guidance returns. - The change does not alter the CLI argument parser. ## Model Used OpenAI Codex, GPT-5, tool use, code execution, and repository review assistance. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- .agents/skills/paperclip-page/README.md | 8 +- doc/CLI.md | 792 ++++++++++-------- doc/DATABASE.md | 2 +- doc/DEVELOPING.md | 14 +- doc/HERMES_GATEWAY_ONBOARDING.md | 14 +- docs/adapters/claude-local.md | 2 +- docs/adapters/codex-local.md | 2 +- docs/api/secrets.md | 4 +- docs/cli/control-plane-commands.md | 76 +- docs/cli/overview.md | 14 +- docs/cli/setup-commands.md | 2 +- docs/deploy/deployment-modes.md | 2 +- docs/deploy/local-development.md | 2 +- docs/deploy/secrets.md | 8 +- docs/deploy/tailscale-private-access.md | 2 +- docs/feedback-voting.md | 4 +- .../board-operator/experimental-features.md | 2 +- docs/guides/openclaw-docker-setup.md | 4 +- .../__tests__/cli-invocation-safety.test.ts | 345 ++++++++ .../__tests__/private-hostname-guard.test.ts | 28 +- server/src/adapters/hermes-gateway-doc.ts | 2 +- .../src/middleware/private-hostname-guard.ts | 20 +- server/src/routes/access.ts | 12 +- server/src/services/company-export-readme.ts | 2 +- skills/paperclip/SKILL.md | 2 + ui/src/lib/agent-onboarding-prompt.ts | 4 +- ui/src/pages/CompanyExport.tsx | 2 +- 27 files changed, 899 insertions(+), 472 deletions(-) create mode 100644 server/src/__tests__/cli-invocation-safety.test.ts diff --git a/.agents/skills/paperclip-page/README.md b/.agents/skills/paperclip-page/README.md index cfe2d87882..47cf6d0f1e 100644 --- a/.agents/skills/paperclip-page/README.md +++ b/.agents/skills/paperclip-page/README.md @@ -493,12 +493,12 @@ Create secrets from environment variables so values do not land in shell history export PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID="$(jq -r '.AccessKey.AccessKeyId' /tmp/paperclip-page-uploader-key.json)" export PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY="$(jq -r '.AccessKey.SecretAccessKey' /tmp/paperclip-page-uploader-key.json)" -pnpm paperclipai secrets create \ +pnpm exec paperclipai secrets create \ --company-id \ --name paperclip-page-aws-access-key-id \ --value-env PAPERCLIP_PAGE_AWS_ACCESS_KEY_ID -pnpm paperclipai secrets create \ +pnpm exec paperclipai secrets create \ --company-id \ --name paperclip-page-aws-secret-access-key \ --value-env PAPERCLIP_PAGE_AWS_SECRET_ACCESS_KEY @@ -532,7 +532,7 @@ host `AWS_PROFILE` identity for the entire agent run: Create or update the company skill from this package: ```bash -pnpm paperclipai skills create \ +pnpm exec paperclipai skills create \ --company-id \ --name "Paperclip Page" \ --slug paperclip-page \ @@ -543,7 +543,7 @@ pnpm paperclipai skills create \ Attach it to an agent: ```bash -pnpm paperclipai skills agent sync \ +pnpm exec paperclipai skills agent sync \ --company-id \ --skill paperclip-page ``` diff --git a/doc/CLI.md b/doc/CLI.md index 5c75aeb4e8..c5eff04686 100644 --- a/doc/CLI.md +++ b/doc/CLI.md @@ -6,6 +6,56 @@ Paperclip CLI now supports both: - instance setup/diagnostics (`onboard`, `doctor`, `configure`, `env`, `allowed-hostname`, `env-lab`) - control-plane client operations (issues, approvals, agents, activity, dashboard) +## Security: safe invocation for content-bearing arguments + +Use `pnpm exec paperclipai` for any command whose argument can hold untrusted or +semi-trusted content. Untrusted content includes issue text, comment bodies, +Markdown, pasted snippets, and model output. `pnpm exec` runs the installed +binary directly. It passes the argument as an inert `argv` value. It does not +run a shell over the value. + +Do not use `pnpm paperclipai` for a content-bearing argument. The safe form and +the unsafe form differ only by the `exec` keyword, so read the command with +care. `pnpm paperclipai` is a `package.json` script. `pnpm` runs the argument +through `/bin/sh` first. The shell interprets these metacharacters before the +CLI starts: + +- command substitution: a backtick pair or `$( )` +- variable expansion: `$NAME` or `${NAME}` (this can leak a secret value into the persisted argument) +- other shell syntax: `; | & < >` + +A crafted value can run an arbitrary command as the invoking user. A crafted +value can also expand an environment variable into the stored argument. No +CLI-side check stops this, because the shell runs before `cli/src` starts. + +Safe forms: + +- `pnpm exec paperclipai ` — the documented default +- `npx paperclipai ` — also injection-safe; use it when no local + install is present. `npx` fetches the package from the npm registry when the + binary is not installed locally, so a mistyped name can start a network + fetch. `pnpm exec` never does this; it runs only the locally installed binary. +- `node /dist/index.js ` — trusted direct-binary call + +Unsafe forms for a content-bearing argument: + +- `pnpm paperclipai ` +- `pnpm run