Skip to content

Fix agent resume, command injection and GitHub CLI probing in terminals - #388

Open
0x92 wants to merge 5 commits into
dcouple:mainfrom
0x92:fix/agent-terminal-robustness
Open

Fix agent resume, command injection and GitHub CLI probing in terminals#388
0x92 wants to merge 5 commits into
dcouple:mainfrom
0x92:fix/agent-terminal-robustness

Conversation

@0x92

@0x92 0x92 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Three independent fixes to agent terminals. None of them is a feature; all three are about a failure that looks like the agent misbehaving.

1. Resumed sessions came back empty. Every spawned terminal inherited the launching process's environment wholesale. When Pane itself is started from inside a Claude Code session — a terminal, a script, a dev run — that includes CLAUDE_CODE_CHILD_SESSION, and Claude Code responds by switching transcript persistence off ("Transcript saving is off — inherited CLAUDE_CODE_CHILD_SESSION marker"). Nothing is written, so the next claude --resume <id> fails with "No conversation found with session ID" and the pane comes back blank after a restart, with nothing on screen explaining why. Those markers are now stripped at spawn: every terminal Pane starts is a new top-level session, so the launching agent's identity is always wrong to pass on.

2. The resume command sometimes ran as laude. A terminal that has just printed its prompt can lose the first character it is given — ConPTY switches input modes around the prompt, and a resize arriving in the same moment makes it likelier. The command then fails as "command not found" and reads as if Pane sent nonsense. It shows up on restart, exactly when a session is trying to resume its agent. The command is now preceded by a byte that costs nothing either way: Ctrl-U clears the input line, and at a fresh prompt there is nothing to clear. Whatever the shell swallows is not part of the command; the command follows 60 ms later.

3. Two red console lines per session, per refresh, on machines without gh. PR enrichment ran gh pr list on every git status refresh regardless of whether the GitHub CLI exists. Availability is now probed once per execution context — keyed by WSL distribution, since the host and a distro have separate PATHs — and the lookup itself is silent. A repository with no GitHub remote, or an unauthenticated gh, is a normal state for a local project, not something to shout about.

Notes

Both new modules (agentSessionEnv, terminalInjection) sit next to terminalPanelManager rather than inside it, so they can be unit-tested without pulling in the database singleton it depends on.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read the CONTRIBUTING.md guidelines
  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have run pnpm typecheck and pnpm lint locally
  • I have tested the Electron app locally with pnpm electron-dev

Critical Areas Modified

  • State management/IPC events

Additional Notes

Tests: 5 for environment stripping, 4 for the injection sequence, and the existing PR-enrichment tests now count gh pr list lookups rather than every command, since the availability probe adds one.

Verified in the running app: a session whose agent had been resumed after a restart, on a host where Pane itself was launched from inside a Claude Code session.

0x92 added 2 commits August 23, 2026 12:20
Two independent fixes to agent terminals, both about noise and silent
failure rather than features.

Resume: every spawned terminal inherited the launching process's
environment wholesale. When Pane itself is started from inside a Claude
Code session — a terminal, a script, a dev run — that includes
CLAUDE_CODE_CHILD_SESSION, and Claude Code responds by switching transcript
persistence off. Nothing is written, so the next `claude --resume <id>`
fails with "No conversation found" and the pane comes back empty after a
restart, with nothing on screen explaining why. Those markers are now
stripped at spawn: every terminal Pane starts is a new top-level session,
so the launching agent's identity is always wrong to pass on.

GitHub CLI: PR enrichment ran `gh pr list` on every git status refresh. On
a machine without `gh` that is two console errors per session per refresh
for something that was never going to work. Availability is now probed once
per execution context (keyed by WSL distribution, since the host and a
distro have separate PATHs) and the lookup itself is silent — a repository
with no GitHub remote, or an unauthenticated `gh`, is a normal state.

Tests: environment stripping (5); the PR enrichment tests now count
lookups rather than every command, since the probe adds one.
A terminal that has just printed its prompt can lose the first character
it is given. ConPTY switches input modes around the prompt, and a resize
arriving in the same moment makes it likelier. The symptom is a command
that runs with its first letter missing — `laude --resume <id>`, which
fails as "command not found" and reads as if Pane sent nonsense. It shows
up on restart, exactly when a session is trying to resume its agent.

The command is now preceded by a byte that costs nothing either way:
Ctrl-U, which clears the input line and at a fresh prompt has nothing to
clear. Whatever the shell swallows, it is not part of the command. The
command follows 60ms later, once that window has passed.

Kept in its own module so it can be unit-tested without dragging in the
database singleton that terminalPanelManager depends on.

@parsakhaz parsakhaz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Request changes — one shell-injection path remains in the PR-enrichment command
Counts: Must Fix: 1 (security: 1) · Should Fix: 0 · pass 1/3

Must Fix (blocks merge; loop back to Implement)

  • MF-1 (security) — quote the branch name before interpolating it into the dashboard's gh pr list shell command · main/src/ipc/dashboard.ts:688 · import and apply escapeShellArg(branchName), matching GitStatusManager · violates "new issue"
    • Evidence: git check-ref-format 'refs/heads/$(id)' exits 0, proving Git accepts command-substitution syntax in branch names; the touched command interpolates branchName raw, while main/src/services/gitStatusManager.ts:638 uses escapeShellArg(branchName).
    • Failure scenario: opening dashboard branch info for a repository checked out on a branch such as $(id) causes the shell to execute id before invoking gh; a malicious repository can replace id with an arbitrary available command.

Praise

  • main/src/services/gitStatusManager.ts:638 correctly escapes the same branch argument in the status-enrichment path.
  • Focused main-process tests passed: 4 files, 64 tests.

@parsakhaz
parsakhaz force-pushed the fix/agent-terminal-robustness branch from 184a390 to 39bcc51 Compare August 23, 2026 19:42

@parsakhaz parsakhaz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review complete: MF-1 is fixed in e6b68d1, and the rebased head passes root typecheck, root lint, and the focused 64-test suite.

@parsakhaz

Copy link
Copy Markdown
Member

Completed the requested three-pass review and pushed the rebased branch at 39bcc516.

Pass 1: Review

  • Rebased the two PR commits onto current main, resolving the Git status test and terminal environment conflicts to preserve both sides.
  • Posted a request-changes review for a real shell-injection defect in the touched dashboard gh pr list command.
  • Fixed it by applying the existing escapeShellArg boundary in e6b68d11 (fix(review): escape dashboard PR branch name).

Pass 2: Simplify

  • Replaced widened environment dictionaries with the repository's existing inferred type-predicate pattern.
  • Removed test-only exports and redundant type assertions while preserving observable coverage.
  • Scoped the injected command as immutable and removed its non-null assertion.
  • Removed an unused exported injection constant caught by Knip.
  • Committed as 4b40e229 (refactor(simplify): tighten terminal helper boundaries).

Pass 3: Refactor

  • Moved agent-session environment sanitization to the main-process utility boundary.
  • Reused it in both TerminalPanelManager and the legacy AbstractCliManager spawn path. This closes the same transcript-persistence failure for legacy Claude resume sessions.
  • Deleted 89 lines of unreachable, commented dashboard implementation.
  • Committed as 39bcc516 (refactor: centralize agent session environment).

Verification

  • Before simplify: root pnpm typecheck passed; 64 focused tests passed; root lint exposed four blocking PR findings.
  • After simplify and deep refactor: root pnpm typecheck passed; root pnpm lint passed, including Oxlint, ESLint, advisory anti-slop checks, boundary conformance, and Knip.
  • Focused main tests: 4 files, 64/64 passed before and after simplify.
  • Full main suite: 68/69 files and 630 tests passed. Two unrelated network-backed skillCacheManager tests exceeded their 5-second timeout during the concurrent full run. The isolated file rerun with --testTimeout=15000 passed 7/7.
  • UI automation was not run because the PR touches no frontend or renderer files.

Follow-ups

  • Add direct dashboard IPC tests, then consolidate its live GitHub PR lookup with GitStatusManager so both paths share availability and result caching.
  • Add a close-before-delay fake-timer test, then track and cancel pending terminal injection timers with terminal lifecycle cleanup.

Left for parsa

  • Let the refreshed CI checks finish and review the force-updated history.
  • Optionally repeat the Windows ConPTY resume smoke test and one legacy Claude resume smoke test.
  • Merge personally when satisfied. No merge was performed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants