Skip to content

feat(pty): add shellSetup command for per-project terminal initialization#992

Merged
arnestrickmann merged 2 commits intogeneralaction:mainfrom
chrishoffman:feat/shell-setup
Feb 20, 2026
Merged

feat(pty): add shellSetup command for per-project terminal initialization#992
arnestrickmann merged 2 commits intogeneralaction:mainfrom
chrishoffman:feat/shell-setup

Conversation

@chrishoffman
Copy link
Contributor

Adds a shellSetup field to .emdash.json that runs a shell command in every terminal (agent and plain) before the session starts. Useful for activating virtual environments, switching Node versions, or any other per-project shell setup.

The original inspiration is to prevent the spinning that that the agents do to figure out how to configure your environment. This could be done manually in the launch terminal but there was no way to configure it in the agent terminal.

Here is my setup for emdash:
Screenshot 2026-02-19 at 10 10 06 PM

@vercel
Copy link

vercel bot commented Feb 20, 2026

@chrishoffman is attempting to deploy a commit to the General Action Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link

greptile-apps bot commented Feb 20, 2026

Greptile Summary

Adds shellSetup field to .emdash.json that runs a shell command in every terminal (agent and plain) before the session starts. Solves the problem of agents spending time figuring out environment configuration (activating venvs, switching Node versions, etc.) by allowing users to configure it once per project.

Implementation:

  • New getShellSetup() method in LifecycleScriptsService reads the config
  • resolveShellSetup() in ptyIpc handles lookups from both worktree and project root
  • ptyManager integrates shellSetup into PTY spawn for both provider CLIs and plain shells
  • UI adds input field in ConfigEditorModal with state management

Key behavior:

  • For provider CLIs: runs shellSetup && cli_command; exec shell
  • For plain shells: runs shellSetup; exec shell -il
  • Follows existing pattern from lifecycle scripts (user-provided shell commands are trusted)

Confidence Score: 4/5

  • Safe to merge with minor consideration for shell command injection risk inherent to the feature's design
  • Implementation follows established patterns for lifecycle scripts and properly handles config resolution from both worktree and project root. The shellSetup commands are directly interpolated into shell command strings, which is correct for the intended functionality but does mean .emdash.json is a trusted config file (like package.json scripts). The two flagged "logic" issues are actually false positives - escaping would break the feature.
  • Pay attention to src/main/services/ptyManager.ts - the shell command construction with shellSetup should be understood before merging

Important Files Changed

Filename Overview
src/main/services/LifecycleScriptsService.ts Added getShellSetup() method to read shellSetup config from .emdash.json - clean implementation following existing patterns
src/main/services/ptyIpc.ts Added resolveShellSetup() helper to look up shellSetup from worktree or project root, properly handles both committed and uncommitted configs
src/main/services/ptyManager.ts Integrated shellSetup into PTY spawn logic for both provider CLIs and regular shells - uses && for providers and ; for shells, then execs back into shell
src/renderer/components/ConfigEditorModal.tsx Added UI input field for shellSetup with proper state management, validation, and JSON serialization

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User configures shellSetup in .emdash.json] --> B[PTY spawn requested]
    B --> C[resolveShellSetup reads config]
    C --> D{Config found?}
    D -->|Yes| E[Pass shellSetup to startPty]
    D -->|No| F[Start PTY without shellSetup]
    E --> G{Provider CLI or plain shell?}
    G -->|Provider CLI| H[Run: shellSetup && cli_command; exec shell]
    G -->|Plain shell| I[Run: shellSetup; exec shell -il]
    H --> J[Agent terminal ready]
    I --> J
    F --> K[Start shell with -il flags]
    K --> J
Loading

Last reviewed commit: e41fdcc

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +933 to +935
const chainCommand = shellSetup
? `${shellSetup} && ${commandString}; exec ${resumeShell}`
: `${commandString}; exec ${resumeShell}`;
Copy link

Choose a reason for hiding this comment

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

shellSetup is interpolated directly into shell command without escaping, creating command injection risk if .emdash.json is compromised

The shellSetup string from user config is concatenated directly into shell commands. While .emdash.json is a project config file, if it's compromised (e.g., malicious PR, supply chain attack), the arbitrary shell command executes with user privileges. Consider escaping using quoteShellArg() from src/main/utils/shellEscape.ts, or add validation/warnings in the config editor since this is inherently dangerous.

Suggested change
const chainCommand = shellSetup
? `${shellSetup} && ${commandString}; exec ${resumeShell}`
: `${commandString}; exec ${resumeShell}`;
const escapedShellSetup = shellSetup ? quoteShellArg(shellSetup) : null;
const chainCommand = escapedShellSetup
? `${escapedShellSetup} && ${commandString}; exec ${resumeShell}`
: `${commandString}; exec ${resumeShell}`;

Comment on lines +947 to +950
if (shellSetup) {
const cFlag = base === 'fish' ? '-ic' : base === 'sh' ? '-lc' : '-lic';
const resumeShell = `'${useShell.replace(/'/g, "'\\''")}' -il`;
args.push(cFlag, `${shellSetup}; exec ${resumeShell}`);
Copy link

Choose a reason for hiding this comment

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

Same command injection issue here - shellSetup needs escaping

Suggested change
if (shellSetup) {
const cFlag = base === 'fish' ? '-ic' : base === 'sh' ? '-lc' : '-lic';
const resumeShell = `'${useShell.replace(/'/g, "'\\''")}' -il`;
args.push(cFlag, `${shellSetup}; exec ${resumeShell}`);
if (shellSetup) {
const cFlag = base === 'fish' ? '-ic' : base === 'sh' ? '-lc' : '-lic';
const resumeShell = `'${useShell.replace(/'/g, "'\\''")}' -il`;
args.push(cFlag, `${quoteShellArg(shellSetup)}; exec ${resumeShell}`);

@arnestrickmann
Copy link
Contributor

Nice addition and clean implementation @chrishoffman Thanks!

@arnestrickmann arnestrickmann merged commit 958acc4 into generalaction:main Feb 20, 2026
1 of 2 checks passed
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