SkillTotal

Is @earendil-works/pi-coding-agent safe?

No malicious indicators - review capabilities before installing
Notable — review in context (capabilities are not malware):
  • 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

npm_package · npm:@earendil-works/pi-coding-agent
LOW
0
/ 100 risk score
Snapshot · scanned Sep 29, 2026 · @earendil-works/pi-coding-agent@0.99.1 · engine 0.53.0 / ruleset 60

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.

Capabilities — what this component can do (not a risk score):
delegated authenticationfilesystem readfilesystem writemcp tools detectednetwork egressshell execution

Behavioral traits

How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.

Tool surface
Tool Usage
Execution authority
Tool Access Control / Direct Tool Access
Filesystem reach
Tool Execution Context
Network egress
Interaction & Communication / Direct Communication
Delegated authentication
Tool Execution Context / User Delegated Credentials

Findings (6)

HIGHNode.js shell/command executionST-SHELL-NODE

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.

MEDIUMNode.js filesystem readST-FS-NODE-READ

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.

MEDIUMNode.js filesystem write/deleteST-FS-NODE-WRITE

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.

MEDIUMNode.js network egressST-NET-NODE

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.

LOWDelegated authentication (OAuth 2.0 / OIDC)ST-AUTH-DELEGATED

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.

LOWMCP tool surface detectedST-MCP-DETECTED

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 …
{"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 component

How we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →