Summary
Codeg's Pi ACP integration automatically writes a fresh workspace as trusted in Pi's user-level trust.json before starting Pi. A repository can therefore commit .pi/extensions/*.js or .ts; when a user creates or restores an active Pi conversation for that repository, Pi loads the project extension and executes its module top level without a project-resource approval prompt.
This is arbitrary JavaScript execution with the local Codeg/Pi process user's permissions. It is not an OS privilege escalation and it is not a claim that merely launching Codeg with no Pi tab executes a repository. The minimum trigger is running Codeg and creating or restoring an active Pi session whose working directory is the repository.
Affected Version
Verified on upstream main at commit df7a872de44546277e4c49cfe9d173c631161dc6 (v0.24.0, 2026-08-11). The earlier reviewed commit 7c48df5ac4aa56912436983447af5b1ba061f3de contains the same behavior. Codeg's current registry uses pi-acp@0.0.33 and launches the installed pi runtime through that adapter.
I did not find a SECURITY.md, SECURITY.txt, or equivalent security policy in the repository or at the repository's GitHub security URL. This report is therefore based on the observed trust-boundary behavior and does not assume a project-specific bounty scope.
Reproduction
The following uses only a temporary directory and a marker file. The extension does not read credentials, access the network, or launch another process.
Repository contents
Create a temporary repository with this file at .pi/extensions/marker.js:
const fs = require("node:fs");
fs.writeFileSync(
process.env.POC_MARKER,
"project extension imported\n",
"utf8",
);
module.exports = () => {};
The file's top-level code is the observation point. It runs if Pi imports the project extension.
Codeg trigger
- Start Codeg
v0.24.0 or the current main build.
- Open/select the temporary repository as the conversation working directory.
- Select the Pi agent and create a conversation, or restore an active persisted Pi conversation for that directory.
- No initial model prompt or terminal/tool call is required.
- Observe that the path named by
POC_MARKER is created with the contents project extension imported.
The expected user-visible operation is simply Pi session creation/connection. The relevant Codeg option is enabled by default; setting the per-agent environment value PI_ACP_TRUST_WORKSPACE=0 disables Codeg's trust seeding.
Direct isolated runtime control
The following script is a complete Pi-side control that demonstrates the trust decision being consumed. It uses Pi 0.84.1, the current npm release at the time of verification, and can be run without a model provider by using RPC/offline mode. It models the exact trust.json write performed by Codeg's latest source.
set -eu
POC_ROOT="$(mktemp -d)"
npm install --prefix "$POC_ROOT/npm" --no-save --no-package-lock \
@earendil-works/pi-coding-agent@0.84.1 pi-acp@0.0.33
mkdir -p "$POC_ROOT/workspace/.pi/extensions" \
"$POC_ROOT/agent-no-trust" "$POC_ROOT/agent-codeg-trust"
printf '%s\n' \
'const fs = require("node:fs"); fs.writeFileSync(process.env.POC_MARKER, "project extension imported\n", "utf8"); module.exports = () => {};' \
> "$POC_ROOT/workspace/.pi/extensions/marker.js"
run_pi() {
agent_dir="$1"
marker="$2"
if [ "$3" = trusted ]; then
node -e '
const fs = require("node:fs");
const path = require("node:path");
const workspace = fs.realpathSync(process.argv[1]);
fs.writeFileSync(
path.join(process.argv[2], "trust.json"),
JSON.stringify({ [workspace]: true }, null, 2) + "\n",
);
' "$POC_ROOT/workspace" "$agent_dir"
fi
(cd "$POC_ROOT/workspace" &&
HOME="$POC_ROOT/home-$3" \
PI_CODING_AGENT_DIR="$agent_dir" \
POC_MARKER="$marker" \
node "$POC_ROOT/npm/node_modules/@earendil-works/pi-coding-agent/dist/cli.js" \
--mode rpc --no-session --offline </dev/null >/dev/null)
if [ -f "$marker" ]; then printf 'loaded\n'; else printf 'not-loaded\n'; fi
}
printf 'without trust: '
run_pi "$POC_ROOT/agent-no-trust" "$POC_ROOT/no-trust.marker" untrusted
printf 'with Codeg-equivalent trust seed: '
run_pi "$POC_ROOT/agent-codeg-trust" "$POC_ROOT/codeg-trust.marker" trusted
Expected output:
without trust: not-loaded
with Codeg-equivalent trust seed: loaded
An additional no-seed run with PI_ACP_TRUST_WORKSPACE=0 at the Codeg layer produces the same not-loaded result as the first control.
Observed Results
I ran the isolated marker test with three groups:
| Group |
Workspace trust before Pi startup |
Marker |
| No trust entry |
No matching trust.json entry |
Not created |
| Codeg-equivalent seed |
Canonical workspace mapped to true |
Created |
| Opt-out control |
No seed, modeling PI_ACP_TRUST_WORKSPACE=0 |
Not created |
The successful marker was 27 bytes and contained project extension imported. The Pi process ran in offline RPC mode and no provider request was made.
Codeg's latest focused Rust tests also passed all five pi_trust_seed tests. I did not launch the full Codeg desktop/server UI in this environment; the dynamic control isolates the Pi import sink while the Codeg unit tests execute the current trust-seeding implementation.
Source Evidence
At df7a872d:
src-tauri/src/acp/connection.rs:776-785 checks the Pi launch preflight and then calls seed_pi_workspace_trust(cwd, runtime_env) before constructing the ACP agent.
src-tauri/src/commands/acp.rs:5096-5122 defines PI_ACP_TRUST_WORKSPACE; any value other than exactly 0, including absence, enables the behavior.
src-tauri/src/commands/acp.rs:5124-5161 canonicalizes the session directory, reads Pi's trust.json, and inserts a boolean true for a fresh directory without asking the user or inspecting project resources.
src-tauri/src/commands/acp.rs:12191-12205 contains a test whose expected behavior is that a fresh workspace “must be marked trusted.”
src-tauri/src/acp/connection.rs:1193-1210 resolves the session working_dir and passes it into agent construction; src-tauri/src/acp/connection.rs:1102-1114 sets the child process current directory to that workspace.
src/hooks/use-connection-lifecycle.ts:218-228 automatically calls connConnect when an active tab has a working directory. The same path is used after persisted tabs are restored.
src-tauri/src/web/handlers/acp.rs:77-113 forwards working_dir into the connection manager.
src-tauri/src/acp/registry.rs:611-636 registers Pi as pi-acp@0.0.33, whose child runtime is the installed pi binary.
On the Pi side, @earendil-works/pi-coding-agent@0.84.1 documents that project extensions live in .pi/extensions/ and that extensions run with full system access. Its runtime discovers cwd/.pi/extensions, resolves the project trust state, and loads the extension when the matching trust entry is true. With no trust entry, the runtime excludes the project extension.
Why Existing Protection Does Not Cover This
Codeg's trust seed is a path-only boolean written immediately before Pi starts. It is not a user approval of the repository's effective extension set. I found no Codeg-side approval or receipt bound to the canonical workspace, repository revision, extension file contents, extension symlink targets, or the final project-resource set. I also found no execution-time revalidation that would invalidate the trust decision after a repository change.
Pi's own project trust mechanism correctly distinguishes untrusted from trusted project resources. The issue is that Codeg silently converts a fresh workspace into the trusted state before Pi evaluates those resources.
Suggested Remediation
Do not write trust.json: true automatically for a workspace solely because Codeg is connecting an agent to it. Keep fresh project resources untrusted by default and show an explicit first-use approval before Pi imports .pi/extensions or other executable project resources.
The approval should display and bind at least the canonical workspace, user, launcher surface, repository revision or effective resource digest, each extension/module identity and content hash, and referenced or symlinked targets. Re-check that identity immediately before Pi startup and invalidate the receipt when project resources change. Keep PI_ACP_TRUST_WORKSPACE=0 as an opt-out, but do not make the default a permanent path-only trust decision.
Summary
Codeg's Pi ACP integration automatically writes a fresh workspace as trusted in Pi's user-level
trust.jsonbefore starting Pi. A repository can therefore commit.pi/extensions/*.jsor.ts; when a user creates or restores an active Pi conversation for that repository, Pi loads the project extension and executes its module top level without a project-resource approval prompt.This is arbitrary JavaScript execution with the local Codeg/Pi process user's permissions. It is not an OS privilege escalation and it is not a claim that merely launching Codeg with no Pi tab executes a repository. The minimum trigger is running Codeg and creating or restoring an active Pi session whose working directory is the repository.
Affected Version
Verified on upstream
mainat commitdf7a872de44546277e4c49cfe9d173c631161dc6(v0.24.0, 2026-08-11). The earlier reviewed commit7c48df5ac4aa56912436983447af5b1ba061f3decontains the same behavior. Codeg's current registry usespi-acp@0.0.33and launches the installedpiruntime through that adapter.I did not find a
SECURITY.md,SECURITY.txt, or equivalent security policy in the repository or at the repository's GitHub security URL. This report is therefore based on the observed trust-boundary behavior and does not assume a project-specific bounty scope.Reproduction
The following uses only a temporary directory and a marker file. The extension does not read credentials, access the network, or launch another process.
Repository contents
Create a temporary repository with this file at
.pi/extensions/marker.js:The file's top-level code is the observation point. It runs if Pi imports the project extension.
Codeg trigger
v0.24.0or the currentmainbuild.POC_MARKERis created with the contentsproject extension imported.The expected user-visible operation is simply Pi session creation/connection. The relevant Codeg option is enabled by default; setting the per-agent environment value
PI_ACP_TRUST_WORKSPACE=0disables Codeg's trust seeding.Direct isolated runtime control
The following script is a complete Pi-side control that demonstrates the trust decision being consumed. It uses Pi
0.84.1, the current npm release at the time of verification, and can be run without a model provider by using RPC/offline mode. It models the exacttrust.jsonwrite performed by Codeg's latest source.Expected output:
An additional no-seed run with
PI_ACP_TRUST_WORKSPACE=0at the Codeg layer produces the samenot-loadedresult as the first control.Observed Results
I ran the isolated marker test with three groups:
trust.jsonentrytruePI_ACP_TRUST_WORKSPACE=0The successful marker was 27 bytes and contained
project extension imported. The Pi process ran in offline RPC mode and no provider request was made.Codeg's latest focused Rust tests also passed all five
pi_trust_seedtests. I did not launch the full Codeg desktop/server UI in this environment; the dynamic control isolates the Pi import sink while the Codeg unit tests execute the current trust-seeding implementation.Source Evidence
At
df7a872d:src-tauri/src/acp/connection.rs:776-785checks the Pi launch preflight and then callsseed_pi_workspace_trust(cwd, runtime_env)before constructing the ACP agent.src-tauri/src/commands/acp.rs:5096-5122definesPI_ACP_TRUST_WORKSPACE; any value other than exactly0, including absence, enables the behavior.src-tauri/src/commands/acp.rs:5124-5161canonicalizes the session directory, reads Pi'strust.json, and inserts a booleantruefor a fresh directory without asking the user or inspecting project resources.src-tauri/src/commands/acp.rs:12191-12205contains a test whose expected behavior is that a fresh workspace “must be marked trusted.”src-tauri/src/acp/connection.rs:1193-1210resolves the sessionworking_dirand passes it into agent construction;src-tauri/src/acp/connection.rs:1102-1114sets the child process current directory to that workspace.src/hooks/use-connection-lifecycle.ts:218-228automatically callsconnConnectwhen an active tab has a working directory. The same path is used after persisted tabs are restored.src-tauri/src/web/handlers/acp.rs:77-113forwardsworking_dirinto the connection manager.src-tauri/src/acp/registry.rs:611-636registers Pi aspi-acp@0.0.33, whose child runtime is the installedpibinary.On the Pi side,
@earendil-works/pi-coding-agent@0.84.1documents that project extensions live in.pi/extensions/and that extensions run with full system access. Its runtime discoverscwd/.pi/extensions, resolves the project trust state, and loads the extension when the matching trust entry istrue. With no trust entry, the runtime excludes the project extension.Why Existing Protection Does Not Cover This
Codeg's trust seed is a path-only boolean written immediately before Pi starts. It is not a user approval of the repository's effective extension set. I found no Codeg-side approval or receipt bound to the canonical workspace, repository revision, extension file contents, extension symlink targets, or the final project-resource set. I also found no execution-time revalidation that would invalidate the trust decision after a repository change.
Pi's own project trust mechanism correctly distinguishes untrusted from trusted project resources. The issue is that Codeg silently converts a fresh workspace into the trusted state before Pi evaluates those resources.
Suggested Remediation
Do not write
trust.json: trueautomatically for a workspace solely because Codeg is connecting an agent to it. Keep fresh project resources untrusted by default and show an explicit first-use approval before Pi imports.pi/extensionsor other executable project resources.The approval should display and bind at least the canonical workspace, user, launcher surface, repository revision or effective resource digest, each extension/module identity and content hash, and referenced or symlinked targets. Re-check that identity immediately before Pi startup and invalidate the receipt when project resources change. Keep
PI_ACP_TRUST_WORKSPACE=0as an opt-out, but do not make the default a permanent path-only trust decision.