---
issue: 96
issue_title: "Subagent permission forwarding broken for all major pi-subagent extensions"
---

# Broaden subagent env hint keys for major pi-subagent extensions

## Problem Statement

Permission forwarding relies on `SUBAGENT_ENV_HINT_KEYS` to detect whether the current process is running as a subagent.
Those three keys (`PI_IS_SUBAGENT`, `PI_SUBAGENT_SESSION_ID`, `PI_AGENT_ROUTER_SUBAGENT`) are not set by any of the three major pi-subagent extensions.
As a result, `isSubagentExecutionContext()` returns `false` in child sessions spawned by nicobailon/pi-subagents or HazAT/pi-interactive-subagents, and `resolvePermissionForwardingTargetSessionId()` returns `null` because `PI_AGENT_ROUTER_PARENT_SESSION_ID` is also never set by those extensions.
Any `ask`-state permission in such a headless child session silently denies instead of forwarding the dialog to the parent.

The tintinweb extension runs subagents fully in-process — no env vars are ever set, so detection there cannot rely on env vars at all.
That case is deferred to #29 (event bus RPC).

## Goals

- Broaden `SUBAGENT_ENV_HINT_KEYS` to include the env vars that nicobailon/pi-subagents and HazAT/pi-interactive-subagents actually set in child processes.
- Add a `SUBAGENT_PARENT_SESSION_ENV_CANDIDATES` list covering known parent-session env vars so parent-session resolution succeeds for each extension where that information is available.
- Emit a structured debug/review log entry when parent-session resolution fails so users get an actionable message instead of a silent denial.
- Add tests covering detection and parent-session resolution for each extension's env var pattern.
- Document which extensions are now covered and which remain deferred (tintinweb in-process case → #29).

## Non-Goals

- Fixing the tintinweb in-process subagent case (no child process → no env vars; tracked in #29).
- Proposing or enforcing a shared upstream convention across extensions (tracked in #98).
- Changing the file-based forwarding protocol or polling logic.
- Modifying how `yolo-mode` short-circuits the `ask` path.

## Background

### Permission surfaces involved

This issue touches the **forwarding** path that sits above all permission surfaces: when a permission resolves to `ask` in a headless subagent context the extension must forward the dialog to the parent session rather than blocking or silently denying.
No surface-level rule evaluation changes — only the subagent detection and parent-session resolution steps.

### Relevant modules

`src/permission-forwarding.ts`
: Declares `SUBAGENT_ENV_HINT_KEYS`, `SUBAGENT_PARENT_SESSION_ENV_KEY`, and `resolvePermissionForwardingTargetSessionId()`.
These are the primary targets of this fix.

`src/subagent-context.ts`
: `isSubagentExecutionContext()` iterates `SUBAGENT_ENV_HINT_KEYS` and falls back to session-dir path comparison.
The path-based fallback works only when the session directory happens to be nested under `subagentSessionsDir`, which is path-layout-dependent and fragile for external extensions.

`src/forwarded-permissions/polling.ts`
: `waitForForwardedPermissionApproval()` calls both `isSubagentExecutionContext()` and `resolvePermissionForwardingTargetSessionId()`.
When either returns a falsy result the function logs a forwarding error and returns `{ approved: false, state: "denied" }` — the silent denial.

`tests/subagent-context.test.ts`
: Existing tests cover the three original `SUBAGENT_ENV_HINT_KEYS` and the session-dir path fallback.
New tests for the additional keys go in the same file.

### Extension env var inventory

| Extension                      | Child-process env vars                                                                    | Parent-session env var        |
| ------------------------------ | ----------------------------------------------------------------------------------------- | ----------------------------- |
| nicobailon/pi-subagents        | `PI_SUBAGENT_CHILD`, `PI_SUBAGENT_RUN_ID`, `PI_SUBAGENT_CHILD_AGENT`, `PI_SUBAGENT_DEPTH` | none set (no mechanism today) |
| tintinweb/pi-subagents         | none (in-process, deferred)                                                               | n/a                           |
| HazAT/pi-interactive-subagents | `PI_SUBAGENT_NAME`, `PI_SUBAGENT_ID`, `PI_SUBAGENT_SESSION`, `PI_SUBAGENT_ACTIVITY_FILE`  | none set (no mechanism today) |

Neither extension currently sets a parent-session env var.
Parent-session resolution for these extensions will fail at the `SUBAGENT_PARENT_SESSION_ENV_CANDIDATES` lookup.
The fix improves the error log to surface this explicitly so users can track progress against #98.

## Design Overview

### 1. Broaden env hint keys

Add the known child-indicator vars from each extension to `SUBAGENT_ENV_HINT_KEYS` in `src/permission-forwarding.ts`:

```typescript
export const SUBAGENT_ENV_HINT_KEYS = [
  // pi-agent-router (original)
  "PI_IS_SUBAGENT",
  "PI_SUBAGENT_SESSION_ID",
  "PI_AGENT_ROUTER_SUBAGENT",
  // nicobailon/pi-subagents
  "PI_SUBAGENT_CHILD",
  "PI_SUBAGENT_RUN_ID",
  "PI_SUBAGENT_CHILD_AGENT",
  "PI_SUBAGENT_DEPTH",
  // HazAT/pi-interactive-subagents
  "PI_SUBAGENT_NAME",
  "PI_SUBAGENT_ID",
  "PI_SUBAGENT_SESSION",
  "PI_SUBAGENT_ACTIVITY_FILE",
] as const;
```

This makes `isSubagentExecutionContext()` return `true` for child processes from both extensions without changing the function's signature or logic.

### 2. Add parent-session env var candidates

The existing `SUBAGENT_PARENT_SESSION_ENV_KEY` is a single string (`"PI_AGENT_ROUTER_PARENT_SESSION_ID"`).
Replace it with an ordered array of candidates so `resolvePermissionForwardingTargetSessionId()` can try each in turn:

```typescript
/** Ordered list of env var names to check for the parent session ID. */
export const SUBAGENT_PARENT_SESSION_ENV_CANDIDATES: readonly string[] = [
  // pi-agent-router (original)
  "PI_AGENT_ROUTER_PARENT_SESSION_ID",
] as const;
```

Neither nicobailon nor HazAT currently sets a parent-session env var, so only the original key appears now.
The array design lets a future step (or a #98 adoption) add more candidates without changing call sites.

`resolvePermissionForwardingTargetSessionId()` is updated to iterate the candidates:

```typescript
export function resolvePermissionForwardingTargetSessionId(options: {
  hasUI: boolean;
  isSubagent: boolean;
  currentSessionId?: string | null;
  env?: NodeJS.ProcessEnv;
}): string | null {
  if (options.hasUI) {
    return normalizePermissionForwardingSessionId(options.currentSessionId);
  }
  if (!options.isSubagent) {
    return null;
  }
  for (const key of SUBAGENT_PARENT_SESSION_ENV_CANDIDATES) {
    const resolved = normalizePermissionForwardingSessionId(options.env?.[key]);
    if (resolved) return resolved;
  }
  return null;
}
```

`SUBAGENT_PARENT_SESSION_ENV_KEY` is kept as a deprecated re-export alias for one release so external callers are not broken:

```typescript
/** @deprecated Use SUBAGENT_PARENT_SESSION_ENV_CANDIDATES */
export const SUBAGENT_PARENT_SESSION_ENV_KEY =
  SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0];
```

### 3. Improve the failure log message

In `waitForForwardedPermissionApproval()` in `src/forwarded-permissions/polling.ts`, the existing error message names only `PI_AGENT_ROUTER_PARENT_SESSION_ID`.
Update it to list all candidates and mention the open tracking issue:

```typescript
logPermissionForwardingError(
  deps.logger,
  `Permission forwarding target session could not be resolved. ` +
    `Checked env vars: ${SUBAGENT_PARENT_SESSION_ENV_CANDIDATES.join(", ")}. ` +
    `If you are using nicobailon/pi-subagents or HazAT/pi-interactive-subagents, ` +
    `parent-session forwarding is not yet supported for those extensions (see issue #98).`,
);
```

### Edge cases

- A `PI_SUBAGENT_DEPTH=0` value is a non-empty string and will correctly trigger detection; depth-0 is still a subagent context.
- `PI_SUBAGENT_ACTIVITY_FILE` is a file path string; any non-empty value marks the child as a subagent.
- The session-dir path-based fallback in `isSubagentExecutionContext()` is unchanged and remains as a secondary guard.
- Adding keys increases the surface area of "what counts as a subagent"; this is intentional and aligned with least-privilege (forward/ask rather than silently allow in a falsely-non-subagent context).

### Merge precedence impact

No config-level policy change.
The forwarding path sits above rule evaluation; this fix only changes when the extension *decides* to attempt forwarding rather than what decision it makes.

## Module-Level Changes

`src/permission-forwarding.ts`
: - Replace `SUBAGENT_ENV_HINT_KEYS` tuple with the expanded list.
: - Add `SUBAGENT_PARENT_SESSION_ENV_CANDIDATES` array.
: - Keep `SUBAGENT_PARENT_SESSION_ENV_KEY` as a deprecated alias.
: - Update `resolvePermissionForwardingTargetSessionId()` to iterate candidates.

`src/forwarded-permissions/polling.ts`
: - Update the "could not resolve" error log message to name all candidates and reference #98.

`tests/subagent-context.test.ts`
: - Add detection tests for each new env hint key (nicobailon group and HazAT group).
: - Add a test asserting `SUBAGENT_ENV_HINT_KEYS` contains every key from both groups.

`tests/permission-forwarding.test.ts` *(new file)*
: - Test `resolvePermissionForwardingTargetSessionId()`:

- hasUI=true returns current session ID.
- isSubagent=false returns null.
- isSubagent=true, none of the candidates set → returns null.
- isSubagent=true, first candidate (`PI_AGENT_ROUTER_PARENT_SESSION_ID`) set → returns its value.
- isSubagent=true, first candidate absent but a hypothetical second set → returns second's value (future-proofing).
- Test `SUBAGENT_PARENT_SESSION_ENV_KEY` is still exported and equals the first candidate.

`docs/architecture/target-architecture.md`
: - Update the subagent-detection section to name the three extensions and their env var sets.
: - Note the tintinweb in-process case as deferred to #29.

## TDD Order

### Step 1 — tests: new env hint key detection

File: `tests/subagent-context.test.ts`

Add test cases that `isSubagentExecutionContext()` returns `true` for each newly added key: `PI_SUBAGENT_CHILD`, `PI_SUBAGENT_RUN_ID`, `PI_SUBAGENT_CHILD_AGENT`, `PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_NAME`, `PI_SUBAGENT_ID`, `PI_SUBAGENT_SESSION`, `PI_SUBAGENT_ACTIVITY_FILE`.

Add a "covers all declared SUBAGENT_ENV_HINT_KEYS" guard test that reads the exported array and asserts each key has an individual test.

These tests are **red** until Step 2.

Commit: `test: cover nicobailon + HazAT subagent env hint keys (#96)`

### Step 2 — feat: broaden SUBAGENT_ENV_HINT_KEYS

File: `src/permission-forwarding.ts`

Expand `SUBAGENT_ENV_HINT_KEYS` with the eight new keys.
No other logic changes.

Step 1 tests turn **green**.

Commit: `feat: broaden SUBAGENT_ENV_HINT_KEYS for nicobailon + HazAT extensions (#96)`

### Step 3 — tests: SUBAGENT_PARENT_SESSION_ENV_CANDIDATES and updated resolver

File: `tests/permission-forwarding.test.ts` *(new)*

Cover:

- `SUBAGENT_PARENT_SESSION_ENV_CANDIDATES` is an array containing `"PI_AGENT_ROUTER_PARENT_SESSION_ID"`.
- `SUBAGENT_PARENT_SESSION_ENV_KEY` equals `SUBAGENT_PARENT_SESSION_ENV_CANDIDATES[0]` (deprecated alias still present).
- `resolvePermissionForwardingTargetSessionId` with hasUI=true, isSubagent=false, isSubagent=true+none set, isSubagent=true+first candidate set.

These tests are **red** until Step 4.

Commit: `test: cover SUBAGENT_PARENT_SESSION_ENV_CANDIDATES and resolver (#96)`

### Step 4 — feat: add SUBAGENT_PARENT_SESSION_ENV_CANDIDATES, iterate in resolver

File: `src/permission-forwarding.ts`

- Add `SUBAGENT_PARENT_SESSION_ENV_CANDIDATES`.
- Keep `SUBAGENT_PARENT_SESSION_ENV_KEY` as a deprecated alias.
- Update `resolvePermissionForwardingTargetSessionId()` to iterate the candidates array.

File: `src/forwarded-permissions/polling.ts`

- Update the forwarding-failure log message to list all candidates and reference #98.

Step 3 tests turn **green**.

Commit: `feat: add SUBAGENT_PARENT_SESSION_ENV_CANDIDATES, iterate in resolver (#96)`

### Step 5 — docs: update target architecture

File: `docs/architecture/target-architecture.md`

Document the three extensions, their env vars, and the tintinweb in-process deferral.

Commit: `docs: update target-architecture subagent detection for #96`

## Risks and Mitigations

| Risk                                                                                                                           | Mitigation                                                                                                                                                                                                                                             |
| ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| New env hint keys over-match — a non-subagent process happens to have one of these vars set and is wrongly treated as headless | `PI_SUBAGENT_DEPTH=0` and similar are specific to these extensions; over-match risk is low. The consequence of a false positive is that the extension tries to forward rather than silently allow, which still prompts the user — not a silent bypass. |
| Could this silently weaken a permission?                                                                                       | No. Broadening detection makes more sessions *attempt* forwarding rather than silently denying. The only failure mode is a forwarding attempt that cannot resolve a parent session, which already emits a denial — not an allow.                       |
| `SUBAGENT_PARENT_SESSION_ENV_KEY` removal breaks external callers                                                              | Kept as a deprecated alias for at least one release.                                                                                                                                                                                                   |
| Parent-session resolution still fails for nicobailon and HazAT (no parent-session env var)                                     | The improved error message makes this explicit and points to #98. The silent-denial behavior is unchanged for this specific sub-case until #98 lands.                                                                                                  |
| `PI_SUBAGENT_SESSION` from HazAT is the *child's* session ID, not the parent's                                                 | It is added to `SUBAGENT_ENV_HINT_KEYS` (detection only), not to `SUBAGENT_PARENT_SESSION_ENV_CANDIDATES` (resolution). No confusion possible.                                                                                                         |

## Open Questions

- **#98 adoption**: Once nicobailon and HazAT adopt a shared parent-session env var, add it to `SUBAGENT_PARENT_SESSION_ENV_CANDIDATES`.
  Plan is intentionally array-shaped to make that a one-line change.
- **`PI_SUBAGENT_DEPTH=0`**: Depth-0 could mean "top-level orchestrator in a subagent run".
  If that case should be excluded from subagent detection, a depth check could be added — deferred until there is a concrete user report.
- **tintinweb in-process**: No env var approach can fix this; deferred to #29 (event bus RPC).
