Skip to content

fix(code): apply SessionStart hook env to UI git/gh commands#1915

Merged
jonathanlab merged 2 commits into
mainfrom
posthog-code/respect-sessionstart-env-in-git-buttons
May 19, 2026
Merged

fix(code): apply SessionStart hook env to UI git/gh commands#1915
jonathanlab merged 2 commits into
mainfrom
posthog-code/respect-sessionstart-env-in-git-buttons

Conversation

@posthog
Copy link
Copy Markdown
Contributor

@posthog posthog Bot commented Apr 28, 2026

Problem

The Commit and Create PR buttons run git/gh from the main process (via simple-git and execGh), and both were inheriting bare process.env. That ignored the env updates the Claude Agent SDK applies per session through SessionStart-style hooks — the SDK writes each hook's output to <CLAUDE_CONFIG_DIR>/session-env/<sessionId>/<event>-hook-<N>.sh and sources those files before each of its own bash tool commands.

Most visibly: when a user has the Secretive code-signing hook installed and launches PostHog Code from the Dock (so the parent inherits the default macOS launchd `SSH_AUTH_SOCK`), the agent can sign commits but the UI Commit / Create PR buttons cannot — the `SSH_AUTH_SOCK` repoint the hook performs never reaches the git subprocess they spawn.

Changes

Mirror the SDK's session-env behavior in the main process so UI-triggered git/gh sees the same env the agent does:

  • `apps/code/src/main/services/session-env/loader.ts` — `loadSessionEnvOverrides(sessionId)` sources the SDK's `-hook-N.sh` files via `bash -c '... ; env -0'`, parses the result, and returns the diff against `process.env` (skipping shell internals like `BASH_*`, `SHLVL`, `PWD`).
  • `AgentService.getSessionEnvForTask(taskId)` — resolves the most recent active session for a task and returns its overrides.
  • `@posthog/git` — added optional `env?: Record<string, string>` to `execGh`, `ExecuteOptions`, and `GitSagaInput`. The operation-manager merges it on top of `getCleanEnv()` for the simple-git subprocess.
  • `GitService.commit` / `createPr` — look up the session env when a `taskId` is present and thread it through commit, push, publish, and `gh pr create`. `createPr` resolves it once and passes it via an internal `envOverride` field on the commit options to avoid re-spawning bash for each step.

Test plan

  • Loader unit tests pass (`pnpm --filter code test src/main/services/session-env/loader.test.ts`) — covers happy path, `printf %q` quoted values, multi-file ordering, ignored filenames, parent-env de-duping, missing dir, and bash failure.
  • `pnpm typecheck` and `pnpm --filter code test` pass.
  • Manual: with Secretive's `setup-code-signing.sh` SessionStart hook installed and the app launched from the Dock, click Commit on a task — commit succeeds and the signature is present (`git log --show-signature -1`).
  • Manual: click Create PR in the same condition — the underlying commit signs cleanly and `gh pr create` runs with the hook env.
  • Manual: tasks with no active session (e.g. before the user has prompted the agent) still commit/PR using bare `process.env`.

Created with PostHog Code

The Commit and Create PR buttons run git/gh in the main process via
`simple-git` and `execGh`, both of which were inheriting bare `process.env`.
That ignored the env updates the Claude Agent SDK applies for each session
via SessionStart-style hooks (which the SDK writes as
`<event>-hook-N.sh` files under `<CLAUDE_CONFIG_DIR>/session-env/<sessionId>/`
and sources before its own bash tool commands).

Most visibly, when a user has the Secretive code-signing hook installed and
launches PostHog Code from the Dock (so the parent process inherits the
default macOS launchd `SSH_AUTH_SOCK`), the agent can sign commits but the
UI buttons cannot — the same `SSH_AUTH_SOCK` repoint never reached the git
subprocess they spawn.

Mirror the SDK's behavior in the main process:

- New `loadSessionEnvOverrides(sessionId)` util sources the SDK's hook files
  via bash and returns the env diff (skipping shell internals).
- `AgentService.getSessionEnvForTask(taskId)` resolves the most recent active
  session for a task and returns its overrides.
- Plumb an optional `env` through `@posthog/git` (`execGh`, `GitSagaInput`,
  `ExecuteOptions`) so it reaches both the simple-git client and `gh`.
- `GitService.commit` and `createPr` look up the session env when a `taskId`
  is present and thread it through commit / push / publish / `gh pr create`.

Generated-By: PostHog Code
Task-Id: 86938233-5c7d-4285-b996-6bd9cf09b57c
@jonathanlab jonathanlab force-pushed the posthog-code/respect-sessionstart-env-in-git-buttons branch from 405ea77 to e259eb5 Compare May 19, 2026 13:47
@jonathanlab jonathanlab marked this pull request as ready for review May 19, 2026 14:06
@jonathanlab jonathanlab enabled auto-merge (squash) May 19, 2026 14:06
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 19, 2026

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/git/src/operation-manager.ts:97-101
`GIT_OPTIONAL_LOCKS: "0"` is set before `...options?.env`, so any session hook that exports `GIT_OPTIONAL_LOCKS` (or even an innocuous hook that happens to re-export it as part of its full env dump) would silently override the `"0"` for every read operation. Since `BASH_INTERNAL_VARS` does not filter git-specific vars, this is a real exposure. The constant should be the last write so it always wins for reads.

```suggestion
    const env = {
      ...getCleanEnv(),
      ...options?.env,
      GIT_OPTIONAL_LOCKS: "0",
    };
```

### Issue 2 of 2
apps/code/src/main/services/agent/service.ts:1000-1004
The `.filter((s) => !!s.config.sessionId)` guarantees every item in `candidates` has a truthy `sessionId`, making the subsequent `if (!session?.config.sessionId)` branch dead code — it can only be reached when `candidates` is empty (i.e. `session` is `undefined`). Simplify to a plain presence check.

```suggestion
    const candidates = this.listSessions(taskId)
      .filter((s) => !!s.config.sessionId)
      .sort((a, b) => b.lastActivityAt - a.lastActivityAt);
    const session = candidates[0];
    if (!session) return {};
```

Reviews (1): Last reviewed commit: "Merge branch 'main' into posthog-code/re..." | Re-trigger Greptile

Comment on lines +97 to +101
const env = {
...getCleanEnv(),
GIT_OPTIONAL_LOCKS: "0",
...options?.env,
};
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.

P1 GIT_OPTIONAL_LOCKS: "0" is set before ...options?.env, so any session hook that exports GIT_OPTIONAL_LOCKS (or even an innocuous hook that happens to re-export it as part of its full env dump) would silently override the "0" for every read operation. Since BASH_INTERNAL_VARS does not filter git-specific vars, this is a real exposure. The constant should be the last write so it always wins for reads.

Suggested change
const env = {
...getCleanEnv(),
GIT_OPTIONAL_LOCKS: "0",
...options?.env,
};
const env = {
...getCleanEnv(),
...options?.env,
GIT_OPTIONAL_LOCKS: "0",
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/git/src/operation-manager.ts
Line: 97-101

Comment:
`GIT_OPTIONAL_LOCKS: "0"` is set before `...options?.env`, so any session hook that exports `GIT_OPTIONAL_LOCKS` (or even an innocuous hook that happens to re-export it as part of its full env dump) would silently override the `"0"` for every read operation. Since `BASH_INTERNAL_VARS` does not filter git-specific vars, this is a real exposure. The constant should be the last write so it always wins for reads.

```suggestion
    const env = {
      ...getCleanEnv(),
      ...options?.env,
      GIT_OPTIONAL_LOCKS: "0",
    };
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1000 to +1004
const candidates = this.listSessions(taskId)
.filter((s) => !!s.config.sessionId)
.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
const session = candidates[0];
if (!session?.config.sessionId) return {};
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.

P2 The .filter((s) => !!s.config.sessionId) guarantees every item in candidates has a truthy sessionId, making the subsequent if (!session?.config.sessionId) branch dead code — it can only be reached when candidates is empty (i.e. session is undefined). Simplify to a plain presence check.

Suggested change
const candidates = this.listSessions(taskId)
.filter((s) => !!s.config.sessionId)
.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
const session = candidates[0];
if (!session?.config.sessionId) return {};
const candidates = this.listSessions(taskId)
.filter((s) => !!s.config.sessionId)
.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
const session = candidates[0];
if (!session) return {};
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/agent/service.ts
Line: 1000-1004

Comment:
The `.filter((s) => !!s.config.sessionId)` guarantees every item in `candidates` has a truthy `sessionId`, making the subsequent `if (!session?.config.sessionId)` branch dead code — it can only be reached when `candidates` is empty (i.e. `session` is `undefined`). Simplify to a plain presence check.

```suggestion
    const candidates = this.listSessions(taskId)
      .filter((s) => !!s.config.sessionId)
      .sort((a, b) => b.lastActivityAt - a.lastActivityAt);
    const session = candidates[0];
    if (!session) return {};
```

How can I resolve this? If you propose a fix, please make it concise.

@jonathanlab jonathanlab merged commit ee6e14f into main May 19, 2026
15 checks passed
@jonathanlab jonathanlab deleted the posthog-code/respect-sessionstart-env-in-git-buttons branch May 19, 2026 14:14
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.

1 participant