Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/coding-agent/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@ async function readPipedStdin(): Promise<string | undefined> {
});
}

/** Env vars the interactive client sends to the daemon so extensions can access them. */
const CLIENT_ENV_KEYS = [
"HERDR_ENV",
"HERDR_PANE_ID",
"HERDR_SOCKET_PATH",
"HERDR_TAB_ID",
"HERDR_WORKSPACE_ID",
] as const;

function collectClientEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const key of CLIENT_ENV_KEYS) {
const value = process.env[key];
if (value !== undefined) {
env[key] = value;
}
}
return env;
}

function collectSettingsDiagnostics(
settingsManager: SettingsManager,
context: string,
Expand Down Expand Up @@ -1006,6 +1026,7 @@ async function createDaemonInteractiveConnection(options: {
config: options.config,
sessionPath: options.sessionPath,
continueRecent: options.continueRecent,
env: collectClientEnv(),
});
if (!response.success) {
throw new Error(response.error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ export interface DaemonAgentConnectionOptions {
closeClientOnDispose?: boolean;
}

/** Env vars the interactive client sends to the daemon so extensions can access them. */
const CLIENT_ENV_KEYS = [
"HERDR_ENV",
"HERDR_PANE_ID",
"HERDR_SOCKET_PATH",
"HERDR_TAB_ID",
"HERDR_WORKSPACE_ID",
] as const;

function collectClientEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const key of CLIENT_ENV_KEYS) {
const value = process.env[key];
if (value !== undefined) {
env[key] = value;
}
}
return env;
}

/**
* AgentConnection adapter for the local daemon JSONL socket transport.
*
Expand Down Expand Up @@ -110,6 +130,7 @@ export class DaemonAgentConnection implements AgentConnection {
supportsExtensionUi: true,
clientId: this.clientId,
capabilities: ["attach_snapshot", "event_sequence", "extension_ui", "slim_attach"],
env: collectClientEnv(),
resumeCursor:
this.lastEventSequence === undefined
? undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface DaemonSocketClient {
detachInput: () => void;
supportsExtensionUi: boolean;
capabilities: Set<DaemonClientCapability>;
/** Environment variables sent by the client (e.g. herdr pane env vars). */
clientEnv?: Record<string, string>;
}

export interface ActiveSessionState {
Expand Down
26 changes: 26 additions & 0 deletions packages/coding-agent/src/modes/daemon/daemon-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,29 @@ import {
restrictDaemonSocketPath,
} from "./daemon-socket.js";

/** Env vars from the client that should be exposed to extensions via process.env. */
const CLIENT_ENV_KEYS = [
"HERDR_ENV",
"HERDR_PANE_ID",
"HERDR_SOCKET_PATH",
"HERDR_TAB_ID",
"HERDR_WORKSPACE_ID",
] as const;

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High daemon/daemon-mode.ts:86

applyClientEnvToProcess writes the client's HERDR_* values into the shared process.env, but it is only called during create and attach. When two clients are attached to the same daemon, the last client to attach overwrites the previous client's values in process.env, and subsequent commands from the other client (e.g. prompt) never re-apply that client's clientEnv before running extensions. This means extensions see whichever pane attached last, not the pane that issued the current command. Consider re-applying the invoking client's clientEnv at the start of each command that may run extensions, or isolate the values per-call rather than mutating shared process.env.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/daemon/daemon-mode.ts around line 86:

`applyClientEnvToProcess` writes the client's `HERDR_*` values into the shared `process.env`, but it is only called during `create` and `attach`. When two clients are attached to the same daemon, the last client to attach overwrites the previous client's values in `process.env`, and subsequent commands from the other client (e.g. `prompt`) never re-apply that client's `clientEnv` before running extensions. This means extensions see whichever pane attached last, not the pane that issued the current command. Consider re-applying the invoking client's `clientEnv` at the start of each command that may run extensions, or isolate the values per-call rather than mutating shared `process.env`.

* Update process.env with the client's environment variables so extensions
* running in the daemon process can access them (e.g. herdr pane identity).
* Only a curated set of keys is applied to avoid leaking unrelated env vars.
*/
function applyClientEnvToProcess(env?: Record<string, string>): void {
if (!env) return;
for (const key of CLIENT_ENV_KEYS) {
if (env[key] !== undefined) {
process.env[key] = env[key];
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium daemon/daemon-mode.ts:91

applyClientEnvToProcess() only sets keys present in env and never clears keys that are absent. When one client with HERDR_* variables creates or attaches first, those values persist in process.env; a later client connecting without some or all of those variables inherits the stale values, so extensions target the previous client's pane/socket instead of the current one. Consider deleting each CLIENT_ENV_KEYS entry from process.env before applying the incoming env so missing keys are cleared.

 function applyClientEnvToProcess(env?: Record<string, string>): void {
+	if (env) {
+		for (const key of CLIENT_ENV_KEYS) {
+			delete process.env[key];
+		}
+	}
 	if (!env) return;
 	for (const key of CLIENT_ENV_KEYS) {
 		if (env[key] !== undefined) {
 			process.env[key] = env[key];
 		}
 	}
 }
Also found in 1 other location(s)

packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts:77

collectClientEnv() only includes keys that are currently defined, so a later non-herdr client sends no entry for a previously-set HERDR_* variable. On the daemon side applyClientEnvToProcess() only overwrites provided keys and never deletes missing ones, so stale pane/workspace values remain in process.env and extensions keep reporting to the old pane after attaching from a client without those vars.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/modes/daemon/daemon-mode.ts around lines 91-98:

`applyClientEnvToProcess()` only sets keys present in `env` and never clears keys that are absent. When one client with `HERDR_*` variables creates or attaches first, those values persist in `process.env`; a later client connecting without some or all of those variables inherits the stale values, so extensions target the previous client's pane/socket instead of the current one. Consider deleting each `CLIENT_ENV_KEYS` entry from `process.env` before applying the incoming `env` so missing keys are cleared.

Also found in 1 other location(s):
- packages/coding-agent/src/modes/agent-connection/daemon-agent-connection.ts:77 -- `collectClientEnv()` only includes keys that are currently defined, so a later non-herdr client sends no entry for a previously-set `HERDR_*` variable. On the daemon side `applyClientEnvToProcess()` only overwrites provided keys and never deletes missing ones, so stale pane/workspace values remain in `process.env` and extensions keep reporting to the old pane after attaching from a client without those vars.


export interface DaemonModeOptions {
socketPath?: string;
defaultSessionConfig: AgentSessionRuntimeConfig;
Expand Down Expand Up @@ -898,6 +921,7 @@ export class AgentDaemon {
}

case "create": {
applyClientEnvToProcess(command.env);
const state = await this.createRuntime(command);
return success(command.id, "create", summaryForActiveSession(state));
}
Expand All @@ -909,6 +933,8 @@ export class AgentDaemon {
}
client.capabilities = normalizeClientCapabilities(command.capabilities, command.supportsExtensionUi);
client.supportsExtensionUi = client.capabilities.has("extension_ui");
client.clientEnv = command.env;
applyClientEnvToProcess(command.env);
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
state.clients.add(client);
client.attachedActiveSessionIds.add(state.activeSessionId);
const result = this.createAttachResult(client, state, command);
Expand Down
17 changes: 14 additions & 3 deletions packages/coding-agent/src/modes/daemon/daemon-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ export interface DaemonAttachClientMetadata {
resumeCursor?: DaemonResumeCursor;
}

/**
* Environment variables sent from the interactive client to the daemon so
* extensions running in the daemon process can access client-side env vars
* (e.g. HERDR_PANE_ID, HERDR_SOCKET_PATH set by herdr on each pane).
*/
export interface DaemonClientEnv {
/** Key-value env vars from the client process that the daemon should expose to extensions. */
env?: Record<string, string>;
}

export interface DaemonReplayInfo {
status: DaemonReplayStatus;
fromSequence?: DaemonEventSequence;
Expand Down Expand Up @@ -146,20 +156,21 @@ export interface DaemonAttachResult {
export type DaemonCommand =
| { id?: string; type: "list"; all?: boolean; cwd?: string; sessionDir?: string }
| { id?: string; type: "list_saved_sessions"; activeSessionId: string; scope: AgentConnectionSavedSessionScope }
| {
| ({
id?: string;
type: "create";
sessionPath?: string;
continueRecent?: boolean;
name?: string;
config?: AgentSessionRuntimeConfig;
}
} & DaemonClientEnv)
| ({
id?: string;
type: "attach";
activeSessionId: string;
supportsExtensionUi?: boolean;
} & DaemonAttachClientMetadata)
} & DaemonAttachClientMetadata &
DaemonClientEnv)
| { id?: string; type: "detach"; activeSessionId?: string }
| { id?: string; type: "kill"; activeSessionId: string }
| { id?: string; type: "rename"; activeSessionId: string; name: string }
Expand Down