Skip to content

[P1][BUG] Chat agent modifies host repo without dirty-worktree preflight check, risking uncommitted code loss #8

Description

@pratik-saptarshi

Summary

The chat/fix agent workflow (ChatSession) is designed to inspect, edit, and commit code changes in the target repository. However, it does not check whether the repository has uncommitted changes before beginning. The system prompt instructs the agent to check git status and ask before staging, but this is a best-effort LLM instruction — not a hard enforcement. If the model misinterprets instructions or encounters an adversarial repo, it can silently overwrite or stage uncommitted work, causing irreversible data loss.

Panel verdict: P1, VERIFIED, EXISTING_DEFECT, consensus.


Affected Files

File Lines Issue
src/server/chat.ts L66–L77 System prompt instructs agent to check git but does not enforce it
src/server/chat.ts L230–L302 runAgent() starts immediately with no preflight
src/server/routes.ts L470–L485 /chat/:slug/:id/messages route has no repo dirty-check before dispatching

Root Cause — Code Evidence

System prompt relies on LLM compliance, not code enforcement (src/server/chat.ts, lines 66–70):

const SYSTEM_PROMPT_TEMPLATE = `
// ...
4. Check git state with \`git status\`. If the repo is dirty with unrelated changes, ASK before staging.
// ...
`
// ↑ This is a soft instruction. An agent running with bypassPermissions can still
//   execute 'git add -A && git commit' without asking if it misreads the instruction.

No preflight in runAgent() (src/server/chat.ts, line 230):

private async runAgent(): Promise<void> {
  this.setStatus('streaming');
  this.abortCtrl = new AbortController();
  // ↑ Agent starts immediately — no git status check, no stash, no user confirmation
  //   before the agent has file-write + shell access to the repo.

Impact

  • Data loss: Uncommitted changes in the target repo (e.g. half-written features, sensitive local configurations) can be overwritten or staged by the agent without warning.
  • Irreversibility: If the agent stages and amends an existing commit, local changes may be unrecoverable without git reflog.
  • Compounding risk: Combined with bypassPermissions (Issue [P0][SECURITY] Unauthenticated loopback RCE via bypassPermissions + missing CORS/CSRF guards #1), the agent has no filesystem restrictions — it can modify any file, not just those in the target repo.

Steps to Reproduce

  1. Open a git repository with uncommitted changes.
  2. Start Probus and initiate a chat fix session for a vulnerability report in that repo.
  3. Send a message like "Please apply the fix and create a PR".
  4. Observe whether the agent checks git status before writing files. In adversarial or rushed scenarios, it may skip this step.

Remediation

Step 1 — Perform a server-side git dirty check before starting the agent

// src/server/chat.ts — add to sendMessage() or runAgent() preamble
import { execFileSync } from 'node:child_process';

function isRepoDirty(repoPath: string): boolean {
  try {
    const out = execFileSync('git', ['status', '--porcelain'], {
      cwd: repoPath,
      encoding: 'utf8',
      timeout: 5000,
    });
    return out.trim().length > 0;
  } catch {
    return false; // not a git repo or git not installed — allow agent to proceed
  }
}

Step 2 — Block or warn before starting the agent on a dirty repo

async sendMessage(text: string): Promise<void> {
  if (this.status === 'streaming') {
    throw new Error('Agent is still responding.');
  }
  // Preflight: warn if repo has uncommitted changes
  if (isRepoDirty(this.repoPath)) {
    // Option A: Hard block (safest)
    throw new Error(
      'Repository has uncommitted changes. Stash or commit them before running the fix agent.'
    );
    // Option B: Soft warning — emit a warning event but continue
    // this.emit({ type: 'error', message: 'Warning: repo has uncommitted changes.' });
  }
  // ... rest of sendMessage unchanged
}

Step 3 — Auto-stash before agent runs (advanced option)

// Stash uncommitted changes, run agent, then offer to pop stash on completion
execFileSync('git', ['stash', 'push', '-m', 'probus-auto-stash'], { cwd: this.repoPath });
// ... run agent ...
// After agent finishes:
// execFileSync('git', ['stash', 'pop'], { cwd: this.repoPath });

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions