Summary
Probus spawns Claude Code agents via runClaudeAgent() and runChatAgent() with no wall-clock timeout. There is no deadline on how long an agent may run, how many tool calls it may make, or how many tokens it may consume. In pathological cases — adversarial repositories, model misalignment, or complex codebases — an agent can enter an infinite read-edit loop, running indefinitely until the user manually clicks Stop (or the API rate limit kicks in). This drains API credits silently.
Panel verdict: P2, VERIFIED, EXISTING_DEFECT (Completeness Audit finding G-06 / VAL-21).
Affected Files
| File |
Lines |
Issue |
src/claude-agent.ts |
L51–L61 |
No timeout option in Options object |
src/server/chat-agent.ts |
L50–L59 |
No timeout option in chat agent Options |
src/scanner.ts |
L156–L188 |
runAgent() passes signal? but never creates a timeout signal |
Root Cause — Code Evidence
No timeout in scanner agent options (src/claude-agent.ts, lines 51–61):
const options: Options = {
cwd,
model,
env: { ...process.env, ...env } as Record<string, string | undefined>,
abortController,
includePartialMessages: true,
permissionMode: 'bypassPermissions',
settingSources: [],
// ↑ No maxTokens, no timeout, no maxTurns — agent can run forever
};
runAgent() accepts a signal but never creates a timeout one (src/scanner.ts, lines 156–188):
async function* runAgent(
prompt: string,
cwd: string,
model: string,
signal?: AbortSignal, // ← caller provides signal, but callers never add a timeout
logFile?: string,
stageLabel?: string,
): AsyncGenerator<...> {
// ...
for await (const ev of runClaudeAgent({ prompt, cwd, model, env, signal, logFile, stageLabel })) {
yield ev;
}
}
Impact
- Credit drain: A single stuck agent on a complex file can consume $10–$100+ in API credits before the user notices.
- UI freeze: The scan/chat interface shows a perpetual loading state with no timeout indicator.
- Cascading scans: In high-parallelism mode (
parallel=16), 16 concurrent agents can all get stuck simultaneously.
Steps to Reproduce
- Scan a repository containing a deliberately complex or adversarially crafted source file (e.g. a 10,000-line minified JS bundle).
- Observe: the agent enters a read-loop trying to process the file, never completing.
- Wait 10 minutes — the scan is still running with no timeout.
Remediation
Step 1 — Add per-agent wall-clock timeout using AbortSignal.timeout()
// src/claude-agent.ts — in runClaudeAgent()
const MAX_AGENT_DURATION_MS = 5 * 60 * 1000; // 5 minutes per file scan
// Compose the caller's signal with a timeout signal:
const timeoutSignal = AbortSignal.timeout(MAX_AGENT_DURATION_MS);
const combined = AbortSignal.any([timeoutSignal, ...(signal ? [signal] : [])]);
const options: Options = {
// ...
abortController, // use abortController driven by combined signal
};
// Drive abortController from combined signal:
combined.addEventListener('abort', () => abortController.abort(combined.reason), { once: true });
Step 2 — Add a max token budget per agent
If the Claude Code SDK supports a token cap:
const options: Options = {
// ...
maxTokens: 50_000, // cap per-agent invocation
};
Step 3 — Add configurable timeout to scan options
Expose agentTimeoutSeconds in the POST /api/scans body (defaulting to 300s) and thread it through to runAgent():
const agentTimeout = Math.min(body.agentTimeoutSeconds ?? 300, 600) * 1000;
// Pass to scanner as AbortSignal.timeout(agentTimeout)
Step 4 — Show elapsed time in the UI
Display per-file elapsed time in the scan progress view so users can see when an agent is running unusually long and manually abort it.
References
Summary
Probus spawns Claude Code agents via
runClaudeAgent()andrunChatAgent()with no wall-clock timeout. There is no deadline on how long an agent may run, how many tool calls it may make, or how many tokens it may consume. In pathological cases — adversarial repositories, model misalignment, or complex codebases — an agent can enter an infinite read-edit loop, running indefinitely until the user manually clicks Stop (or the API rate limit kicks in). This drains API credits silently.Panel verdict: P2, VERIFIED, EXISTING_DEFECT (Completeness Audit finding G-06 / VAL-21).
Affected Files
src/claude-agent.tsOptionsobjectsrc/server/chat-agent.tsOptionssrc/scanner.tsrunAgent()passessignal?but never creates a timeout signalRoot Cause — Code Evidence
No timeout in scanner agent options (
src/claude-agent.ts, lines 51–61):runAgent()accepts a signal but never creates a timeout one (src/scanner.ts, lines 156–188):Impact
parallel=16), 16 concurrent agents can all get stuck simultaneously.Steps to Reproduce
Remediation
Step 1 — Add per-agent wall-clock timeout using AbortSignal.timeout()
Step 2 — Add a max token budget per agent
If the Claude Code SDK supports a token cap:
Step 3 — Add configurable timeout to scan options
Expose
agentTimeoutSecondsin thePOST /api/scansbody (defaulting to 300s) and thread it through torunAgent():Step 4 — Show elapsed time in the UI
Display per-file elapsed time in the scan progress view so users can see when an agent is running unusually long and manually abort it.
References