Skip to content

feat(code): add Hooks v2 session runtime - #4915

Closed
Johannes du Plessis (johannes117) wants to merge 1 commit into
mainfrom
johannes117/code/dcd-70-hooks-runtime
Closed

feat(code): add Hooks v2 session runtime#4915
Johannes du Plessis (johannes117) wants to merge 1 commit into
mainfrom
johannes117/code/dcd-70-hooks-runtime

Conversation

@johannes117

@johannes117 Johannes du Plessis (johannes117) commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Closes DCD-70

Hooks v2 now freezes configuration once per session, materializes safe transcript projections, and executes events through a client-owned runtime.


  • Adds the authoritative event capability registry, validated config loading, canonical snapshot hashing, and narrowly scoped legacy migration.
  • Adds versioned per-thread and subagent transcript storage with redaction, private permissions, atomic writes, retention, stable revisions, and path-safe identities.
  • Hardens command execution and reduction with sanitized environments, process-group cleanup, terminal sequence validation, event-aware output policies, deferred-field diagnostics, and evidence-based native/MCP tool mapping.
  • Keeps lifecycle call-site and server/client dispatch wiring out of scope for the next PRs.
Test plan
  • uv run --group test pytest tests/unit_tests/hooks — 101 passed
  • make lint
  • make check_imports
  • Full make test — 10,319 passed; 3 terminal-width assertions failed in untouched Rich rendering tests

Freeze hook configuration per session and materialize safe transcript projections so lifecycle integrations can execute against one consistent runtime.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions github-actions Bot added dcode Related to `deepagents-code` feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: XL 1000+ LOC labels Jul 21, 2026

@corridor-security corridor-security Bot left a comment

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.

The Hooks v2 runtime unconditionally loads and executes project-local hook commands from {cwd}/.deepagents/hooks.json without any trust gate, enabling arbitrary command execution when a user opens the CLI in a malicious repository. The timeout-bypass finding is a false positive — subprocesses are already started with start_new_session=True, making killpg(pid, ...) correct.

diagnostics: list[HookDiagnostic] = []
merged: dict[HookEvent, list[MatcherGroup]] = {}
loaded_paths: list[Path] = []

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.

The default source list unconditionally includes project-local hook configuration:

sources = (
    tuple(paths)
    if paths is not None
    else (
        project_hooks_path(cwd),
        user_hooks_path(config_dir),
    )
)

Those project hooks can define command handlers executed via asyncio.create_subprocess_shell(). A malicious repository can commit .deepagents/hooks.json with a lifecycle hook to run arbitrary commands when a user opens the CLI in that checkout — no additional interaction required.

Remediation: Require explicit user approval before including project_hooks_path(cwd). Store a fingerprint of the approved file in the user config directory and re-prompt on change; default to user hooks only until the project file is trusted.

Attack Path
  1. Attacker commits .deepagents/hooks.json with a SessionStart command payload.
  2. Victim runs the CLI in the repository; HooksRuntime.create(cwd=cwd) is called.
  3. load_hooks_config adds project_hooks_path(cwd) to sources without any trust check.
  4. _read_hooks_document parses the attacker-controlled file.
  5. run_command_handler passes the command string to asyncio.create_subprocess_shell(), executing the payload with the victim's privileges.

For more details, see the finding in Corridor.

Provide feedback: Reply with whether this is a valid vulnerability or false positive to help improve Corridor's accuracy.

@johannes117

Copy link
Copy Markdown
Contributor Author

Superseded by the smaller Git Town stack in #4916, #4917, and #4918. The original branch remains available as a recovery snapshot.

@open-swe open-swe Bot left a comment

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.

Open SWE Review found 3 potential issues.

Open in WebView Open SWE trace

Comment on lines +130 to +134
self.root = root.expanduser().resolve()
self.retention_revisions = retention_revisions
self._buffers: dict[tuple[str, str | None], _TranscriptBuffer] = {}
self._lock = threading.RLock()
_ensure_private_directories(self.root, self.root)

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.

🟠 Symlinked transcript root escapes the checkout

root.expanduser().resolve() follows a project-controlled .deepagents/transcripts symlink and then treats the resolved destination as the trusted store root. With the default runtime path, a checkout containing .deepagents/transcripts -> /some/outside/dir causes transcript files to be created outside the checkout (and _ensure_private_directories chmods that external directory). I reproduced this with a symlinked project transcript directory: materialize() returned a path under the symlink target and Path.is_relative_to(repo) was false. Reject a pre-existing symlink / enforce that the canonical default root remains beneath the canonical checkout before creating or chmodding anything.

(Refers to lines 130-134)


Your feedback helps Open SWE learn. React with 👍 or 👎 to tell us if this review comment was useful.

Comment on lines +95 to +96
project_hooks_path(cwd),
user_hooks_path(config_dir),

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.

🟡 Home-directory sessions load every hook twice

When cwd is the user's home directory, the project path and default user path are both ~/.deepagents/hooks.json. The loader iterates both entries without deduplicating them, so every matcher group is appended twice and each matching command runs twice. This is especially visible for side-effecting notification and SessionEnd hooks. Canonicalize/deduplicate the source paths while preserving precedence before reading them.

(Refers to lines 95-96)


Your feedback helps Open SWE learn. React with 👍 or 👎 to tell us if this review comment was useful.

Comment on lines +247 to +259
if os.name == "posix" and process.pid is not None:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
except OSError:
with suppress(OSError):
process.kill()
else:
with suppress(OSError):
process.kill()
with suppress(OSError, TimeoutError):
await asyncio.wait_for(process.wait(), timeout=_TERMINATE_WAIT_TIMEOUT)

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.

🟡 Timed-out hooks leave grandchild processes behind

Killing the entire process group with SIGKILL terminates the direct shell before it can reap its children; in container environments whose PID 1 does not reap orphans promptly, those grandchildren remain as zombies. The two new cleanup tests reproduce this here: both test_runner_kills_process_group_on_timeout and ..._on_cancellation fail because os.kill(grandchild_pid, 0) still succeeds after _terminate() returns. This can accumulate process-table entries across timed-out/cancelled hooks. Terminate descendants first (giving the shell a chance to reap), then force-kill/reap the direct child as a fallback.

(Refers to lines 247-259)


Your feedback helps Open SWE learn. React with 👍 or 👎 to tell us if this review comment was useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dcode Related to `deepagents-code` feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: XL 1000+ LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant