Is @earendil-works/pi-coding-agent safe?
- Node.js shell/command execution
- Node.js filesystem read
- Node.js filesystem write/delete
@earendil-works/pi-coding-agent is an AI npm_package analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 6 risky constructs are reported for review. It can: delegated authentication, filesystem read, filesystem write, mcp tools detected, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 0/100 (low).
@earendil-works/pi-coding-agent 0.99.1
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of @earendil-works/pi-coding-agent's authors. Report a false positive.
Behavioral traits
How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.
Findings (6)
The component can run operating-system commands or spawn processes.
import { spawn } from "node:child_process";const proc = spawn(command, args, {import { execFile, spawnSync } from "child_process";const result = spawnSync("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], {import { execSync, spawnSync } from "child_process";const result = spawnSync(shell, commandFromStdin ? args : [...args, command], {const output = execSync(command, {import { spawn } from "child_process";const child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], {import { spawn } from "child_process";const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"] });import { spawn } from "child_process";const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] });import { spawnSync } from "node:child_process";const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });import { spawn } from "node:child_process";const child = spawn(editor, [...editorArgs, filePath], {import { spawn } from "child_process";const proc = spawn("tmux", ["show", "-gv", option], {import { spawn, spawnSync } from "node:child_process";const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });proc = spawn("gh", ["gist", "create", "--public=false", tmpFile]);import { spawn } from "node:child_process";const childProcess = spawn("node", [cliPath, ...args], {import { type ChildProcess, type ChildProcessByStdio, type SpawnOptions, type SpawnOptionsWithStdioTuple, type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns, type StdioNull, type StdioPipe } from "node:child_process";Why it matters: Powerful and often legitimate — confirm the commands aren't built from untrusted input.
Fix: Confirm the command and its arguments are fully controlled and not derived from untrusted input; prefer execFile with an argument array.
The component reads files from disk.
cachedImageBase64 = fs.readFileSync(getBundledInteractiveAssetPath(IMAGE_FILENAME)).toString("base64");const body = fs.readFileSync(tmpFile);
dark: JSON.parse(stripBom(fs.readFileSync(darkPath, "utf-8"))),
light: JSON.parse(stripBom(fs.readFileSync(lightPath, "utf-8"))),
const content = fs.readFileSync(registeredTheme.sourcePath, "utf-8");
const content = fs.readFileSync(themePath, "utf-8");
const content = fs.readFileSync(themePath, "utf-8");
* The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')
* The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')
Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.
Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.
The component writes or deletes files on disk.
fs.writeFileSync(filePath, Buffer.from(image.bytes));
fs.writeFileSync(debugLogPath, debugData);
fs.rmSync(tempDir, { recursive: true, force: true });Why it matters: Usually legitimate, but worth confirming the paths can't be controlled by untrusted input.
Fix: Confirm which files are written/deleted and that paths cannot be influenced by untrusted input.
The component makes outbound network requests.
</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …`)){let eq=line.indexOf("=");eq>0&&(result[line.slice(0,eq)]=line.slice(eq+1))}return result}function extractExtensionVersions(ext,memory){let versionsFnName=`qjs_ext_${ext.name.replace(/-/g,"_")}_versions`,versionsFn=ext.instance.exports[v …var __require=(x=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(x,{get:(a,b)=>(typeof require<"u"?require:a)[b]}):x)(function(x){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+x+'" is n …var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync …
</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync …
const response = await fetch(new URL("/v1/bug-reports", options.gatewayUrl ?? getRadiusGatewayUrl()), {const response = await fetch(`${this.serverUrl}${path}`, { ...init, headers, signal });const response = await fetch(`${this.serverUrl}/models/sse`, { headers, signal });const response = await fetch(`${this.baseUrl}${path}`, {fetch: (input, init) => fetch(input, { ...init, signal: AbortSignal.timeout(REFRESH_REQUEST_TIMEOUT_MS) }),// one-shot commands alive. On Windows, Node can assert after fetch() if process.exit(0)
void fetch(`https://pi.dev/api/report-install?version=${encodeURIComponent(version)}`, {const response = await fetch(url, {const response = await fetch(url, { headers: { "User-Agent": getPiUserAgent(VERSION) } });const response = await fetch(input, signal ? { ...init, signal } : init);Why it matters: Usually legitimate, but confirm the destinations are expected and no sensitive data leaves.
Fix: Confirm the destination hosts are expected and that no sensitive data is sent off-host.
An OAuth 2.0 / OpenID Connect delegated-authentication flow was detected (authorization-code / refresh-token / token-exchange grant, an OIDC authorize/discovery endpoint or id_token, or a delegation library). Tools authenticate with the end user's delegated, scoped credentials rather than a long-lived embedded service credential. (12 occurrence(s) shown as evidence).
</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …var __require=(x=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(x,{get:(a,b)=>(typeof require<"u"?require:a)[b]}):x)(function(x){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+x+'" is n …</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …</html>`}function oauthSuccessHtml(message){return renderPage({title:"Authentication successful",heading:"Authentication successful",message})}function oauthErrorHtml(message,details){return renderPage({title:"Authentication failed",heading …var CANCEL_MESSAGE="Login cancelled",TIMEOUT_MESSAGE="Device flow timed out",SLOW_DOWN_TIMEOUT_MESSAGE="Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync …
Fix: Delegated auth is a lower-blast-radius execution context than an embedded static credential. Confirm the requested scopes are minimal and that tokens are never logged or forwarded off-host.
An MCP tool surface (manifest or tool definitions) was found.
import{Bm25Ranker,CODEMODE_STORE_ENTRY_TYPE,DEFAULT_TOOL_SEARCH_LIMIT,MODEL_GLOBAL_DECLARATIONS,combineUsage,createToolSearchDocument,getCodemodeCallableTools,parseCodemodeSource,renderToolSample,toCodemodeDeclaration,toCodemodeIdentifier}f …pi.registerTool({{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/codemode/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,4BAA4B,EAAE,MAAM,WAAW,CAAC;AAWzD,SAAS,QAAQ,CAAC,EAAgB;IACjC,OAAO,EAAE,CAAC,WAAW,EAAE …pi.registerTool(definition);
pi.registerTool({ ...definition, exposure: "hidden" });pi.registerTool({ ...definition, exposure: "hidden" });pi.registerTool(definition);
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAi …{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAi …{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAi …{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/mcp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG1C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAi …pi.registerTool({ ...createToolSearchToolDefinition({ tools: pi }), defaultActive: false });{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/extensions/tool-search/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,8BAA8B,EAAE,MAAM,WAAW,CAAC;AAE3D,MAAM,UAAU,yBAAyB;IACxC,OAAO,CAAC,EAAE,EAAE,EAAE; …Why it matters: Just context — review which tools it offers and their permissions.
Fix: Review the declared MCP tools and their permissions.
Check your own component
Run the same evidence-backed scan on any MCP server, agent skill, or package.
Scan your own componentHow we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →