Skip to content

Shim installer safety-hardening: cross-session capture leak + uninstall enforcement #814

@michael-wojcik

Description

@michael-wojcik

Context

PR #808 (commit f93e6e40) landed the capture-campaign installer scripts in-repo at pact-plugin/tests/runbooks/install_logging_shim.sh and install_taskcompleted_logging_shim.sh. Live install during the capture experiment revealed a cross-session capture leak that the installer design does not currently guard against.

Empirical failure mode

When the installer modifies a hook file at ~/.claude/plugins/cache/pact-plugin/PACT/<version>/hooks/<event>.py, the modification applies to that hook file globally on the machine. Every Claude Code session using that plugin version invokes the shimmed hook on every fire.

During PR #808's capture experiment, the TaskCompleted shim captured a fire from a parallel "reflectica" Claude Code session on the same machine — NOT our PACT development session. Empirical evidence: 1 of 3 captures had session_id: 3655e8cc-... (not our 7642b0c9-... session).

// Capture from /tmp/pact-hook-stdin-captures/taskcompleted/agent_handoff_emitter/20260520T055047352461Z-pid2350.json
{
  \"hook_event_name\": \"TaskCompleted\",
  \"session_id\": \"3655e8cc-...\",         // OTHER session's ID — not ours
  \"task_id\": \"24\",                       // OTHER session's task
  \"agent_type\": \"PACT:pact-orchestrator\",
  \"cwd\": \"/path/to/other/session/cwd\"   // OTHER session's working directory
}

Privacy + scope concerns

  1. Privacy: captures contain task_description (free-text), cwd (path fingerprint), session_id (correlatable across captures), transcript_path (filesystem path), and full tool I/O for PostToolUse. A user running the shim for one session's debug accidentally captures stdin from ANY concurrent session.
  2. Forgotten installs: nothing currently auto-uninstalls the shim. If the installer runs and the operator forgets the manual cp .preshim.bak hook.py uninstall step, the shim keeps capturing indefinitely for ALL future sessions on this machine.
  3. Cross-user surface: on shared machines (multi-tenant Mac, dev container with multiple users), one user's shim install affects all other users' sessions.

Current safety surface (insufficient)

  • .preshim.bak backup file is created (operator can restore)
  • Try/except: pass in shim Python (no behavioral impact if shim errors)
  • Idempotency check prevents double-install
  • Marker comment in hook file (PACT-PREPARER-LOGGING-SHIM-INSTALLED) makes installs visible to grep

None of these prevent cross-session capture or enforce uninstall.

Proposed mitigations (graduated)

Tier 1 — Documentation hardening (cheap)

  • Add a prominent "⚠️ DEVELOPMENT USE ONLY" banner to both installer scripts + the in-repo runbook (pending-scan-dogfood.md or a new shim-install-runbook.md).
  • Add an explicit warning that captures contain stdin from ALL concurrent Claude Code sessions on the machine.
  • Make the uninstall command the LAST thing the installer prints (already does — make it more prominent).
  • Document a recommended workflow: install → run capture session → IMMEDIATELY uninstall → THEN promote captures to fixtures.

Tier 2 — Session-scoping (medium)

  • Modify the shim to FILTER captures by session_id BEFORE writing: read the installing-session's session_id at install time, embed it as a constant in the shim, and only capture stdin payloads whose session_id matches.
  • Concrete sketch:
# In install_taskcompleted_logging_shim.sh, at install time:
INSTALLING_SESSION_ID = $(grep -oE 'session_id: [a-f0-9-]+' \"$HOME/.claude/projects/...latest-jsonl\" | head -1)

# Embedded in shim Python:
shim_template = '''
import json as _shim_json
_SHIM_SCOPED_SESSION_ID = \"__SCOPED_SESSION_ID_LITERAL__\"
try:
    _shim_buffer = _shim_sys.stdin.read()
    _shim_data = _shim_json.loads(_shim_buffer) if _shim_buffer else {}
    if _shim_data.get(\"session_id\") == _SHIM_SCOPED_SESSION_ID:
        # only capture our session's fires
        ...write capture...
    # Always replay stdin so the hook still works
    _shim_sys.stdin = _shim_io.StringIO(_shim_buffer)
except Exception:
    pass
'''

Tier 3 — Auto-uninstall (heavy)

  • Wire the shim install lifecycle into PACT session-end: /PACT:wrap-up or session-end hook checks for installed shims and uninstalls them.
  • Add a pact-plugin/tests/runbooks/uninstall_*_shim.sh companion script (currently the uninstall is one-line cp instructions in the install script's output).
  • Track installed-shim state in ~/.claude/teams/<team>/shim-state.json so PACT knows what's installed and can clean up.

Recommended initial scope

Tier 1 (immediate) + Tier 2 (medium effort, high value). Tier 3 (heavy) deferred to follow-up.

Acceptance criteria

  • Both installer scripts carry a prominent "⚠️ DEVELOPMENT USE ONLY" banner + cross-session-capture warning.
  • Shim Python filters captures by session_id (only capturing payloads matching the installing-session's session_id).
  • uninstall_*_shim.sh companion scripts land in pact-plugin/tests/runbooks/.
  • In-repo runbook documents the install → capture → uninstall lifecycle explicitly.
  • (Stretch) PACT session-end auto-checks for installed shims and warns + offers to uninstall.

Cross-refs

Severity

type: security (privacy class, not auth boundary — same-machine same-user threat model; foot-gun protection rather than adversarial-boundary). priority: high because the installer scripts are now in-repo and discoverable; without hardening, any contributor running them risks the same leak.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions