53 Commits
Author SHA1 Message Date
7cb46d9fed feat(hooks): add FileChanged notifications for local sessions (#13969)
* feat(hooks): add FileChanged notifications for local sessions

* fix(ci): preserve shell gate coverage and trim transcript data

* fix(ci): flush debug prompts and isolate git fixtures

* fix(hooks): respect ACP watcher ownership and skill registration

* test: stabilize hosted recovery and failed spawn fixtures

* fix(ci): budget the complete hosted integration fork

* fix(runtime-broker): accept MariaDB CSI guard metadata

* test: align schema baseline and hook dispatch waits

* fix(ci): allow hosted shared fork to finish all classes

* fix(hooks): preserve file watching across errors and cwd changes

* fix(ci): isolate reference archive extraction per run

* test(ci): keep reference archive checks dependency-free

* fix(hooks): constrain recursive watches and repair CI

* test(web-shell): synchronize verified connection return

* fix(ci): correct runtime broker callback indentation

* test(web-shell): match the managed workspace registration label

Keep the remote workspace registration smoke checks in sync with the service label added by unified workspace navigation. Preserve exact text matching and all registration and replay assertions.

* fix(ci): reduce standalone runtime fixture overhead

* fix(hooks): ignore implicit atomic-write temporary files

* test(managed-agent): recover initial node port conflicts

* fix(hooks): observe parents before discovery anchors disappear

* test(managed-agent): await stream cleanup before recovery teardown

* test(managed-agent): align hosted shell fixture capabilities

* test(managed-agent): let OS choose initial harness port

* fix(ci): restore runner-local Java cache permissions

* test(runtime-broker): use native execution profile paths

* test(managed-agent): bind valid workspace upload probes

* test(managed-agent): reuse the upload probe digest fixture

* test(managed-agent): bind hook fault listeners to available ports

* test(managed-agent): align the native profile path fixture

* test(managed-agent): Wait for projection refusal retries

* ci: Prepare daemon Java cache before Node preflight

* test(managed-agent): check channel model calls after delivery

* fix(hooks): observe explicitly registered symlink targets

* test(managed-agent): separate consumption from relay snapshots

* test(managed-agent): target tamper injection by resource key

* test(managed-agent): exhaust attestation EOF retry in fault gates

* fix(ci): bound RocketMQ release downloads

* test(tui): await the restored assistant before capturing history

* fix(ci): use a checksum-pinned RocketMQ mirror

* fix(ci): provision missing browser test fonts

* fix(ci): quote the font probe environment

* fix(ci): prepare browser fonts without elevation

* test(core): compare complete prune reports without ordering

* fix(ci): repair only runner-local Java tool caches

* test(managed-agent): qualify fixture JDBC templates

* fix: isolate reconnect hooks and cancellation test fixtures

Skip chat initialization so MCP reconnect cannot re-enable file hooks, and keep cancellation fixtures isolated from the background worktree scanner.

* fix(ci): tolerate a missing Java cache helper on resumed releases

Resuming an existing release checks out a tag that may predate the helper, which failed all five Java jobs before setup-java ran.

* fix: finish the main merge for harness deadlines and workflow size

Use the renamed settle-timeout accessor in the merged hosted-harness tests,
and record sdk-java.yml's new size after the resumed-release cache guard.

* style(managed-agent): restore import grouping in merged verification tests

* test(managed-agent): require consumed child result mirrors

* test(web-shell): center repository settings capture

---------

Co-authored-by: yiliang114 <[email protected]>
Co-authored-by: yungsen.hys <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-11-06 21:43:01 +08:00
406258d66a fix(core): headless project-hook trust requires the folder-trust feature (#16264)
* fix(core): headless hook trust requires the folder-trust feature

The reporter PoC runs headless (--output-format stream-json with a
one-shot prompt), so the unconditional headless exemption left the
reported execution path open on a fresh default environment. Require
the operator to have enabled security.folderTrust.enabled before the
workspace choice counts as implicit trust; with the feature off,
project hooks are withheld in every mode. Embedders enable the feature
in their managed home settings and keep working via their trust store.

Verified locally against the reporter scenario: default fresh home ->
marker absent; feature on + trust-store grant -> marker written.

Refs #14746

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(core): unify the project-hook gate on explicit trust

The headless exemption branch never changed an observable outcome: the
CLI settings layer already drops workspace settings for an undecided
folder once the feature is enabled, and with the feature off the
explicit predicate is false anyway. Replace both branches with the
single explicit-trust rule so interactive and non-interactive callers
share one contract. Behavior is unchanged; verified against the
reporter PoC (default fresh home withheld, trusted path executes).

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): address review findings on the unified hook trust gate

R1-1: the headless feature-off test wrote folderTrust into workspace
settings, where WORKSPACE_RESTRICTED_SETTINGS strips it — the rig user
scope stayed enabled and the test never reached the named gate. Write
the feature-off decision into the user-scope home file instead.

R1-2: the docs sentence overclaimed; IDE-granted trust loads hooks with
the feature off. State the predicate as shipped.

R1-3: headless withholding was silent for the newly gated population.
Emit one stderr line from the non-interactive entry and a debug-log
warn from the registry when the trust gate is why the scope is absent
(getTrustWithheldProjectHooks, diagnostics only).

Verified: reporter PoC withheld with the stderr line present; core
1052 passed; tool-hook integration 14 passed.

Co-authored-by: Qwen-Coder <[email protected]>

* feat(web-shell): surface trust-withheld hooks in the workspace overview

The hooks listing already marks why a project hook is disabled, but the
ACP status serializer dropped the reason, so the web-shell overview only
showed a count while trust-withheld hooks silently did not run. Carry
disabledReason through ServeHookEntry/DaemonHookEntry (optional, older
daemons unaffected), count untrusted hooks in the summary, flag the
facet, and name the remedy in the tooltip (en + zh).

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): pin the trust-withheld hooks tooltip wording

Component-level witness for the untrusted-hooks facet: the row shows
the withheld count and the title names the remedy. The sidebar hooks
facet is opt-in for embedding hosts, so the jsdom suite is the
reachability-independent pin.

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): align fixtures with the surfaced hook state

The workspace-hooks status test now expects the disabledReason the
serializer carries, and the headless-warnings config stub provides the
new getTrustWithheldProjectHooks accessor.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): tolerate partial config stubs in the headless hook notice

The resumed-transcript and stream-json housekeeping tests use minimal
config stubs; optional-chain the two accessors the notice reads, and
provide the accessor on the remaining isInteractive stubs. All 124
llm.test.tsx cases pass.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): make the withheld-hooks surfaces reachable and equivalent

Review findings on the status surfaces: the listing never carried
disabledReason untrusted for settings project hooks (the trust gate
keeps them out of the registry, and only agent/session rows were
trustGated), so the web-shell hint was unreachable; buildHooksListing
now appends the diagnostics block as disabled (untrusted) rows, with a
real-listing test. The headless stderr notice now mirrors the TUI
predicate (raw workspace hooks non-empty, exclude disableAllHooks and
safe/bare), covering the feature-on undecided case the Config accessor
cannot see. The facet value stays language-neutral; the localized
withheld count and remedy live in the tooltip. Core 1035, llm 124,
web-shell sidebar 51 tests pass.

Co-authored-by: Qwen-Coder <[email protected]>

* test(web-shell): browser witness for the withheld-hooks status surface

Real Chromium e2e: open Daemon Status via the rail More popover, switch
to Diagnostics, and assert the workspace.hooks section renders with the
withheld count from a report shaped like the real daemon output.
Screenshot retained as test output.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): fold the deferred approval follow-ups into this PR

Per review direction, land the #16295 items here instead of a follow-up:

- Explicit projectHooksApproved state: a stored approval keeps execution
  authority inside a command-line --yolo session (previously the trust
  gate withheld it because the pending flag is undefined while exempt).
  The flag never doubles as nothing-to-approve; mid-session decisions
  update it. Tests pin yolo+approved executes, yolo+unapproved withholds.
- Display-only raw workspace block: with the feature on and the folder
  untrusted the settings layer drops the workspace scope, so the listing
  could not show the withheld hooks. projectHooksDisplayOnly feeds only
  getTrustWithheldProjectHooks, never the registry; test pins both halves.
- Variable-source wording: the dialog hint now says environment (project
  .env/settings.env or shell), not only the approved project env.

Per-hook approval granularity stays out: it is a feature iteration, not
a correctness fix. Core 1017+1053, cli config 565, approvals 50, llm 127
pass.

Co-authored-by: Qwen-Coder <[email protected]>

* style: prettier the reworded dialog hint

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): /hooks list shows withheld project hooks in ACP/web-shell

The non-interactive /hooks list read only the registry, so an ACP
session (web-shell) with unapproved project hooks answered \"No hooks
configured\" — the user could not see what was withheld or why. The
list now includes the approval/trust diagnostics blocks, marked
disabled (not approved / rejected / folder not trusted). Verified
against a real ACP session: the withheld SessionStart hook is listed
with its reason. Tests pin all three reasons.

Co-authored-by: Qwen-Coder <[email protected]>

* feat(web-shell): explicit project-hooks approval prompt for withheld hooks

Web Shell previously had no interactive surface for the project-hooks
approval gate: users had to run /hooks manually. The daemon now reports
the pending decision (with sanitized hook lines) on /workspace/hooks,
including on a cold daemon and when folder trust withholds the workspace
scope, and a dedicated POST /workspace/hooks/approve route (plus the
workspace-qualified twin) records the same hash-bound decision the TUI
dialog writes. The Web Shell renders an Approve / Keep-disabled banner
from that state. This is deliberately not session/request_permission:
automation clients auto-answer permission prompts, a trust decision must
be an explicit user act, and approving never grants folder trust.

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(web-shell): render hooks approval with the shared ToolApproval card

Replace the custom full-width alert banner with the existing ToolApproval
card in the standard approval slot above the composer, so the hooks trust
decision looks and behaves like every other approval: same panel, option
list, keyboard handling, and safe-default focus. The card supplies a
project_hooks request with decision-specific option labels; ToolApproval
localizes its heading and question for that tool name.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(web-shell): hooks approval probe reads once and polls only while pending

The approval card polled /workspace/hooks unconditionally, adding
background metadata reads the workspace-overview quiet-period contract
forbids. Read once per mount/workspace/settings change, poll only while a
decision is pending, and let the E2E quiet-period test settle the initial
probe before counting sidebar traffic.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): apply the persisted decision once and to every live session

Local review + ponytail over the PR diff:

- The ACP child no longer re-derives and re-writes the approval hash on
  the approve control. The daemon persists the decision for the exact
  block the user reviewed; a second write could bind it to a block that
  changed since the review. The child only applies the decision, to
  every live session config, not just the bootstrap one.
- The raw-workspace review subject hashes the project env files
  regardless of current folder trust, so a decision recorded while
  untrusted still matches after the folder is trusted.
- The headless trust warning no longer doubles with the approval-gate
  warning for a pending block; the registry debug-log duplicate and its
  interface member are removed (the stderr line and listings already
  report the withheld state).
- Drop dead test stubs for an accessor llm.tsx never reads, a redundant
  type member, and a stray blank-line deletion.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): scope the approve decision to the admission-bound workspace

The per-session fan-out matched every live config in the child. Key it
on the admission-bound workspace root (the same root the settings
handlers use) against the request's cwd instead: session/cd moves the
live target dir, and a decision recorded for one workspace must not
clear or set the gate on a session admitted under another. Regression
test covers same-workspace, cd-moved, and foreign-workspace configs.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): bind the project-hooks approval to the reviewed block

The daemon's approve route re-read .qwen/settings.json at click time and
persisted the hash of that fresh read, while the ACP child applied the
decision to the block it rendered at startup. A file change inside that
window (git pull, another session, the workspace's own agent) recorded an
approval for hooks the user never saw, and left the card pending because
the stored hash matched neither side.

The child now persists against the same read that produced the status cell
it rendered, so the reviewed block and the recorded block are the same
object. The daemon only falls back to its own read when no child answered,
which is the cold-daemon case where that read is what the client saw.

Also unbreaks the web-shell lane: App.test.tsx's daemon-react-sdk mock had
no useOptionalWorkspace export, so every App render threw and 1320 tests
failed; and the workspace-overview facet counts no longer include the
per-mount /hooks probe, which broke the exact facet-set assertion.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmw2kfno0dl

* fix(cli): explicit folder trust alone runs project hooks

The startup gate demanded a stored approval even when the folder was
explicitly trusted, contradicting the documented either-or authority:
core's readProjectHooks accepts approval OR explicit trust, but the
pending flag short-circuited the accessor before trust was consulted.
A platform that pre-trusts its managed directories (folderTrust enabled
plus a TRUST_FOLDER/TRUST_PARENT entry) saw every project hook withheld
headless. Clear the pending gate for a trusted folder unless the exact
block was explicitly rejected — a recorded 'no' still wins over trust.

Verified headless on a real fixture: fresh home withholds; trust config
alone fires the hook; a rejected block stays withheld in the same
trusted folder. Gate unit tests cover pending/rejected/approved under
explicit trust.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): bind the approval decision to the rendered block hash end to end

The status cell now publishes pendingApprovalHash, the Web Shell card
echoes it with the decision, and both the daemon route and the ACP child
verify it: the daemon rejects a decision whose hash names a block other
than the current file (409 project_hooks_changed, nothing persisted),
and the child applies a hash-bound decision only to configs holding
that exact startup subject (projectHooksApprovalSubject), leaving any
config with a different block pending. Closes the cold-daemon re-read
window: an approval can no longer bind commands the user never saw.

Verified on a real daemon: render hash -> edit the hook file -> POST
with the rendered hash returns 409 and persists nothing; re-render ->
approve with the fresh hash persists and enables the hook; a real
browser session then fires it. Card tests 5/5 (including hash echo),
approve-control tests 2/2, gate tests 20/20, build + bundle clean.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): close the remaining approval-gate review findings

- Escape on the Web Shell trust card defers without recording a decision
  (TUI semantics): ToolApproval gains escapeDismisses/onDismiss, and the
  card hides until the block hash changes instead of persisting a
  rejection for a habitual keypress.
- The card's options are *_once kinds so the shared safe-default logic
  pre-selects the reject side; a stray Enter on the security gate can
  never grant hook execution. Decision-specific labels stay via the
  option-id i18n keys.
- The hooks-trust card reports into approvalOverlayActive, so the
  composer is dormant while the card owns the keyboard, exactly like a
  tool approval.
- Under command-line YOLO, replacing the hooks block drops the old
  approval authority: an edited block cannot execute on the previous
  block's approval while the gate is exempt.
- The trust-withheld listing surfaces the display-only block even when
  the empty execution block reads the gate as cleared (feature on,
  folder untrusted) — the state the copy exists for.

Tests: core config + hooks-listing 1055/1055, hooks reload 38/38,
ToolApproval + card 99/99 including Escape-defer and reject-default
regressions; build + bundle clean.

Co-authored-by: Qwen-Coder <[email protected]>

* chore: record merge provenance

The main merge (952e0633) refreshed the lint gate; this marker commit
carries the required co-author trailer for the batch.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): propagate trust authority to every gate re-application path

Review round at 2a00fb4d: the startup gate honored explicit trust, but
the auxiliary paths re-read only the approval store and re-pended hooks
that trust already authorized.

- applyProjectHooksApproval (the /hooks reload and YOLO-exit path) now
  applies the same either-or rule as startup: explicit trust clears a
  pending block; only an explicit rejection of the exact block wins.
- The startup gate mirrors Config.isExplicitlyTrustedFolder(): an
  IDE-reported trusted workspace counts even with the feature off.
- The headless trust warning stays quiet when the execution accessor
  actually serves the block (approved or trust-authorized), matching the
  TUI predicate; the test mock now carries workspace.settings.hooks.
- The live status cell and the cold-daemon fallback stop prompting when
  the hooks are not actually withheld (explicit trust / served block),
  so a trusted workspace never sees a card whose reject would stop
  running hooks.
- The card's Escape defer is keyed by workspace identity read at dismiss
  time (no reset effect can race it), clears on workspace switch, and a
  failed decision POST renders an explicit retry notice.
- The withheld /hooks listing filters non-event keys and null entries
  like the registry path; the YOLO replacement comment now states the
  fail-closed behavior instead of promising a re-apply that never runs.

Tests: daemon facade covers cold-daemon pending+hash, trusted no-prompt,
hash-match persist, 409 changed-block persists nothing, not-pending;
gate re-apply under trust; IDE trust with feature off; trusted reload
keeps hooks / untrusted edit withheld. cli 1879/1879, core 1055/1055,
web-shell card+ToolApproval 99/99. Real browser: trusted dir runs hooks
with no card and no approval record; untrusted dir card -> Escape defer
(nothing persisted) -> approve -> hook fires.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: yiliang114 <[email protected]>
2026-11-05 14:42:13 +08:00
d3ef273950 fix(cli): gate project hooks behind an approval dialog (#14748)
* fix(cli): gate project hooks behind an approval dialog

Hooks in a workspace .qwen/settings.json ran at startup with no prompt
whenever folder trust was disabled, which is the default. Gate them like
project MCP servers (#4615): a hash-bound, user-local approval stored in
~/.qwen/hookApprovals.json, asked for in an interactive startup dialog
(ink and OpenTUI) and re-checked on /hooks reload. Headless runs leave
unapproved project hooks unloaded and print a stderr warning.

* fix(cli): address review findings on the project hooks approval gate

- Only a command-line --yolo skips the gate; a workspace can set
  tools.approvalMode itself. disableAllHooks, bare and safe leave it unarmed.
- Key decisions by the project that supplied the hooks, not the live cwd,
  so /cd and --worktree keep approvals; /hooks reload acts only on a
  gated session.
- Treat null and non-runnable hooks blocks as no hooks; validate stored
  status; hash with null prototypes; write the store 0600 with forceMode;
  keep QWEN_CODE_HOOK_APPROVALS_PATH out of project .env files.
- Dialog: sanitize repo-supplied text to one row, show env/headers/
  allowedEnvVars/prompt, cap the list to the available height, Esc
  postpones instead of rejecting; ink /hooks marks withheld hooks disabled.
- Gate the legacy merged-hooks fallback in getUserHooks too.
- Headless warning names the recovery for a rejected block.
- Fix llm.test.tsx stubs, the real-settings reload test and the ACP
  integration tests (pre-approve via a shared helper); add render tests.

* fix(cli): address second review round on the project hooks approval gate

- Hook registry tags a copy of each definition, so an approved block keeps
  its hash in a reused settings instance (ACP daemon)
- /hooks reload re-reads the approvals file; Esc adds a transcript notice
- Truncated hook values are flagged; prompt text gets the full budget
- Dialog keeps at least three hook rows and one row per hook
- Hooks listing and both /hooks browsers show withheld project hooks as
  awaiting approval
- Integration helper keys and hashes through the CLI's own code
- Tests for getSeparatedHookSettings, the headless warning, the project
  .env exclusion, and gate table titles; docs record the hash scope and
  the subagent frontmatter follow-up

* fix(cli): keep hook registry identity; copy project hooks for the gate instead

The registry copy broke managed hook runtimes, which map registry entries back
to their definitions by config identity. Hand Config a clone of the project
hooks block so the settings instance an ACP daemon reuses keeps its hash.

* fix(cli): bind hook approval to the unresolved block; show what runs

- Hash and display the hooks block before ${VAR} resolution so startup and /hooks agree
- Show command env as KEY=value; matcher last; rows cut to width with a marker
- Derive dialog chrome from the wrapped header at the real width
- Neutral "not approved" label (pending and rejected)
- Docs: frontmatter hooks and statusLine are not gated; ACP status has no reason

* fix(cli): read the unresolved hooks block only when the resolved one is gated

* test: approve project hooks in new tests that rely on them

* fix(hooks): bind approval to referenced env values and harden the approval dialog

- Hash and show the values of $VAR/${VAR} a project hook references, so a .env change re-asks
- Read the approvals file fresh, merge on save, and keep hooks withheld when a save fails
- Count only hooks the registry would run; show a prompt hook's model; cut by code point
- Keep the dialog inside the list budget in both renderers, modal over shells in OpenTUI
- Esc dismisses a block for the session; label rejected hooks separately in /hooks
- Re-arm the gate when a --yolo session leaves YOLO; forward the store path into sandboxes

* test(serve): allow the hook approvals path in the sandbox env guard

* fix(hooks): bind project hooks approval to the workspace env sources

- Hash the unresolved block with the workspace's own .env files and settings.env instead of parsing $VAR names from the hook text; the dialog no longer shows env values.
- Gate and list hooks from the resolved block, worded from the unresolved one.
- Re-gate a --yolo session on its first switch out of YOLO.
- Skip the gate under a shell execution sandbox; pass separated hook scopes from the hosted and memory-import callers.
- Approval dialog strings are English-only in both renderers.
- Tests for the rejected label, dialog choices, reload ordering, function hooks and the absent headless warning.

* test(integration): hash project hook approvals with the workspace env sources

* fix(cli): keep Ctrl+C out of focused Ink shell input

Fixes the foreground-shell flake in #15814. Same change as #15859.

* test(managed-agent): stop the verification suite running out of heap on hosted runners

Raise the surefire fork to 40% of RAM and make the lease renewal test wait for renewals instead of counting them in a 150ms window. See #15248.

* fix(cli): re-gate project hooks against the live block when leaving YOLO

The YOLO-exit listener re-applied the startup approval snapshot, so a block
replaced by /hooks reload while YOLO-exempt ran unreviewed after leaving
YOLO. Read the live block through a provider closed over the live
LoadedSettings (the disabledSkillNamesProvider pattern), and keep listening
so re-entering and leaving YOLO re-gates again.

Closes review finding R7-1.

* fix(cli): keep hooks approval options visible on short terminals

The dialog chrome never clamped to the row budget, so on a short terminal
the fixed-height frame clipped Reject and left only the pre-focused
Approve. Cut the description to the rows the chrome and one list row leave,
in both renderers, before shrinking the hook list.

Closes review finding R7-2.

* test(cli): build reload approval fixtures from the real env sources

The fixture pinned an empty env while the production hash reads every
project env file findEnvFiles discovers, including ancestors of the temp
root, so the approval tests went red on runners with an ancestor .env.
Read the same sources in the fixture and plant an ancestor .env so the
hash coverage stays provably sensitive on every machine.

Closes review finding R7-3.

* fix(core): approval is execution authority for project hooks (either-or)

The interactive folder-trust gate merged from main swallowed the
approval decision: with the feature off (default), Approve persisted
but getProjectHooks() still returned undefined, so the dialog promised
execution it could not deliver (review blocking item 1; tmux Cases
1b/7). readProjectHooks() now admits the block when the hash-bound
approval is approved OR the folder is explicitly trusted; pending and
rejected stay withheld, and an ungated interactive session still needs
folder trust. Tests pin interactive + feature-off + approved => runs,
pending/rejected => withheld, ungated untrusted => withheld.

Also from the same review (suggestions): the Ink /hooks browser now
mirrors the execution accessor instead of showing trust-withheld hooks
as enabled; a command containing variable references says it resolves
them from the approved project env; the Esc notice points at restart
instead of implying /hooks can decide.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): align hook status surfaces and docs with either-or authority

The either-or execution gate left three status surfaces behind (review
blocking items 1a-1c): the startup trust warning fired for approved
blocks that actually load, getTrustWithheldProjectHooks() re-listed an
approved block as trust-disabled (duplicate contradictory rows in the
OpenTUI /hooks browser and ACP status), and the Ink browser's enabled
expression showed ungated untrusted sessions as enabled. The warning now
skips approved blocks, the listing accessor shares the execution
predicate (hasApprovedProjectHooks), and the Ink row mirrors
getProjectHooks() directly. Docs state approval-or-trust authority and
the folderTrust-on-untrusted no-dialog case (blocking item 2).

Tests: approved + default trust => no warning and no withheld copy;
ungated untrusted => Ink row disabled; pending => trust row present.

* fix(cli): warn from the execution accessor, not the approval flag

The round-10 warning guard read gated && !pending as "approved", but
pending === false also means "nothing to approve" (an untrusted folder
with the trust feature on contributes no settings). That suppressed the
one signal the user gets in exactly the case the warning exists for
(review blocking item; also the Integration Tests no-AK failure). Skip
the warning only when getProjectHooks() actually serves the block, and
offer approve-or-trust only while a decision is genuinely pending.

Rename the ambiguous core predicate to isProjectHooksApprovalCleared and
document both meanings; getWithheldProjectHooks reads the raw block so
the listing names a pending/rejected decision even when trust also
withholds, with the approval reason outranking the trust reason and no
duplicate rows. Scope the docs' YOLO execution claim to non-interactive
runs.

Tests: folderTrust-on untrusted warning regression (unit + the existing
interactive integration case), approval-reason priority in the listing,
raw-block reachability while pending.

---------

Co-authored-by: 易良 <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-11-04 19:24:58 +08:00
易良andQwen-Coder 35aa4b76ae fix(core): require explicit trust for settings-file project hooks (#16196)
* fix(core): require explicit trust for settings-file project hooks

A repository .qwen/settings.json SessionStart hook ran at startup with
no trust prompt while the folder-trust feature was off by default:
getProjectHooks() consulted isTrustedFolder(), which returns trusted
when the feature is disabled. Gate it on isExplicitlyTrustedFolder()
instead, matching the frontmatter-hook and LSP gates from #16182.
Update the hooks docs, and grant explicit trust in hook test fixtures.

Refs #14746

Co-authored-by: Qwen-Coder <[email protected]>

* style(core): use Array<T> for the tuple-array cast

Co-authored-by: Qwen-Coder <[email protected]>

* test(integration): grant explicit trust in ACP parity fixtures

The ACP parity rig spawns the CLI with its own QWEN_HOME, so the trust
granted by TestRig.setup() never reaches those sessions. With project
hooks now requiring explicit folder trust, the UserPromptSubmit hook
scenarios silently lost their hooks. Seed the scenario home with folder
trust enabled and the rig directory trusted, matching the tool-hook
suites.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): align project-hook trust with the interactive boundary

Follow the Claude Code model: interactive sessions keep the explicit
folder-trust gate, while non-interactive invocations (-p, piped stdin,
ACP/SDK embeddings) treat trust as implicit — the caller chose the
directory and there is no prompt surface. This keeps platform embedders
working without trust-file migration. Surface a startup warning when
interactive project hooks are withheld so the gate is not silent, and
document the headless contract. Revert the integration-fixture trust
grants that the headless exemption makes unnecessary.

Refs #14746

Co-authored-by: Qwen-Coder <[email protected]>

* test(integration): drop ACP parity trust seeding made moot by headless exemption

The parity rig spawns non-interactive ACP sessions, which now treat
trust as implicit, so the seeded trustedFolders.json only perturbed the
request-snapshot comparison (an extra tmp/ tree under .qwen-home).
Restore the fixture to its main-branch shape; all 23 parity cases pass.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): tolerate partial settings shapes in the project-hook warning

The startup warning read settings.workspace.settings.hooks without
optional chaining; AppContainer tests render with a partial
LoadedSettings mock, and a missing workspace scope crashed the effect
before config initialization completed. Guard the lookup; all 272
AppContainer tests pass.

Co-authored-by: Qwen-Coder <[email protected]>

* docs(hooks): resolve trust-gate wording contradictions (R1-5)

Troubleshooting step 3 still stated the legacy trusted-folder rule and
the restart list omitted security.folderTrust and persisted trust
decisions. Align both with the interactive explicit-trust gate and the
headless implicit-trust contract, and mention the withheld-hooks
startup warning and browser state.

Co-authored-by: Qwen-Coder <[email protected]>

* test(integration): drop dead ACP trust seeding in the tool-hook suites

The per-session trustedFolders.json written beside the QWEN_HOME override
is never read: getTrustedFoldersPath() prefers the inherited
QWEN_CODE_TRUSTED_FOLDERS_PATH that TestRig.setup() stubs. Both blocks sit
in front of an --acp spawn, and ACP sessions are non-interactive, so the
headless exemption already loads project hooks without any trust grant.
Remove the writes and the now-unused realpathSync import, restoring both
suites to their base shape.

Refs #14746

* test(cli): pin the folderTrust derivation behind the project-hook gate

The interactive gate is covered by the Config table, but nothing pinned
settings.security.folderTrust.enabled -> Config.folderTrust. Assert that
loadCliConfig derives it and defaults it to false, so a derivation
flipping to true cannot silently arm repo-supplied hooks. Verified the
case goes red when the default is mutated to true.

Refs #14746

* docs(hooks): align trust-boundary descriptions with implementation

Remove the unimplemented claim that withheld project hooks appear in the hook browser. Clarify that loop.md retains the legacy folder-trust predicate while executable project hooks use explicit interactive trust.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): keep folder trust out of workspace reach

The project-hook gate reads security.folderTrust.enabled from merged
settings, so a repository could enable the very flag that authorizes its
own hooks and then reach any pre-existing TRUST_FOLDER or TRUST_PARENT
rule for the directory. Restrict the key to the operator scopes, beside
allowPrivateNetworkHooks, and pin both directions: a workspace can
neither enable the gate nor disarm a user's setting.

Also close the smaller review notes. The startup warning no longer
blames trust when disableAllHooks removed the hook system entirely, and
it now has coverage for the warned and both silent branches. Every hook
fixture ran pre-trusted, so add an interactive integration case that
declines trust and asserts the hooks never fire, the warning names the
remedy, and the tool result still reaches the model unmarked. Drop the
duplicated node:fs import and correct the docs claim that IDE trust
decisions apply live to settings-file hooks.

Refs #14746

Co-authored-by: Qwen-Coder <[email protected]>

* test(cli): align the daemon trust-grant case with the workspace restriction

security.folderTrust is now stripped from workspace scope, so a grant on
a workspace that only self-enables trust reports the disabled state
instead of refusing an ineffective grant. Keep the alias DO_NOT_TRUST
fixture: if the restriction regresses, the grant throws again and this
case goes red (mutation-verified).

Fixes the Test (ubuntu-latest) failure on a89935fa.

* test(cli): follow operator trust in the ancestor-trust case

R1-22 asserted that a workspace folderTrust value reaches the merged
settings, which is the behavior the workspace restriction removes. Drive
the case from the operator's flag instead: the merged value must survive
the repo's opposing declaration, and the ancestor provider must follow
the operator — denying every ancestor on the target's DO_NOT_TRUST rule
when the feature is on, falling back to directory safety when it is off.
Mutation-verified: dropping the restriction turns both cases red.

Fixes the Test (ubuntu-latest) failure on 6851f531.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 23:54:00 +08:00
易良andQwen-Coder 1f09497f45 fix(core): require explicit trust for project frontmatter and LSP (#16182)
* fix(core): require explicit trust for repository execution

* test(integration): explicitly trust skill hook fixtures

* test(lsp): add isExplicitlyTrustedFolder to document-sync mock config

The document-sync tests create a bare Config mock with only
isTrustedFolder. The new explicit-trust gate in NativeLspService and
LspServerManager calls isExplicitlyTrustedFolder(), which is undefined
on the mock, producing TypeError at reload time.

Add isExplicitlyTrustedFolder: () => true alongside the existing
isTrustedFolder mock so the 13 affected cases pass again.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-03 15:44:31 +08:00
1dc74d5838 feat(core): skip re-injecting an unchanged UserPromptSubmit hook context block (#14129)
* feat(core): skip re-injecting an unchanged hook context block

A UserPromptSubmit hook's additionalContext is appended to the user
message on every turn, so a deterministic hook pays for the same block
again on each request and total request tokens grow quadratically over a
session. The bundled auto-recall hooks render a bounded, newest-first
result window, which makes a byte-stable payload easy to hit.

Core is the sole producer of those appended parts and keeps them in
history, so the injection site can compare the wrapped block it is about
to append against what is already there. When a match is found the block
is not appended again and the turn records no hook-context provenance,
keeping the stored transcript consistent with what the model received.
Any payload change fails the equality check and injects normally, so
there is no suppression state to expire.

Refs #14089

* fix(core): prove hook context provenance before skipping a re-injection

Require the shape an injection produces before an identical
UserPromptSubmit context block counts as already delivered. A block now
suppresses a fresh delivery only when a user history entry ends with
exactly that wrapped block and carries at least one part of the user's
own ahead of it.

Tagged text that merely appears — in a model entry, or as a whole user
message, which transcript handling already treats as user-authored — is
not proof the hook delivered it, so those cases inject as before.

Cover the three provenance cases as unit tests next to the positive
control, and re-run the falsification pair at both injection sites.

Refs #14089

* fix(core): key hook-context dedup to an injector-owned receipt

The shape of an ordinary user multipart message is not proof of hook
authorship: an ACP client can submit two text blocks that persist as
exactly the injection shape, byte-for-byte, while the UserPromptSubmit
hook returns nothing. A later genuine delivery of the same payload was
suppressed by that caller-authored entry.

Stamp the part the injector appends with a module-private symbol
receipt and require it, in addition to the exact trailing text, before
an entry counts as already delivered. The symbol cannot cross a JSON
transcript, an ACP prompt block, or model output, so caller-controlled
shape is no longer accepted as the receipt. Resume rebuilds re-stamp
the receipt from the recorded per-turn hookContext provenance, which
only the injector writes; when provenance cannot be established, the
block is injected as before.

Cover the receipt, its resume restore, and the forged-shape negatives
at both injection sites.

Refs #14089

* fix(core): keep hook-context provenance on the live injection path

R1-1: a suppressed turn still records the user-authored displayText, so the
transcript projection survives; only the hook-context provenance is left out.
R1-2: re-stamp the appended block after text flattening, so live history can
prove origin on the next turn the way the resumed path already does.
R1-6: read the resume payload through UserPromptRecordPayload.
R1-10: drop a comment describing a promptText assignment that does not exist.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmve1dj7jft

* docs(design): align the dedup design doc with shipped behaviour

- Problem paragraph: identical bytes only when the dialect omits per-query
  scores; the shipped example copies the score (request-engine.ts:253).
- Risks: suppression is receipt-gated, so compaction and a resume without
  per-turn hookContext provenance re-inject once and re-arm.
- Acceptance criterion: at-most-once holds while the stamped part stays
  trailing; a later append or a provenance-less rebuild re-injects one
  duplicate.
- Validation step 1: name the three shapes the predicate tests actually
  cover; shape-only variants belong to isUserPromptSubmitContextPartText.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmve3ip14fy

* fix(core): keep the hook-context receipt helpers off the root barrel

R1-5: `createUserPromptSubmitContextPart` and
`restoreUserPromptSubmitContextReceipt` are module-internal provenance
primitives. The design doc allows no new public surface beyond the exported
predicate, and the only root-barrel consumer was a CLI test. Drop both from
the root barrel and point that test at the existing `userPromptSubmitContext`
subpath, which is how the ACP production path already imports them.

R1-9: document the suppression next to the reserved-tag explanation in the
user-facing hooks page, which currently states the opposite.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmve5nurig3

* fix(cli): keep the UserPromptSubmit append when the ACP prompt resolved to no parts

R2-1: on the ACP lane a prompt whose only block is dropped (a resource_link
that fails workspace revalidation, or a blank slash-command body) resolves to
an empty part array. Suppressing the byte-identical hook context there left
the model-bound request empty, and `toParts` rejects an empty array with
`partOrString cannot be an empty array`. Suppression now requires at least
one part, which restores the pre-dedup append for that case.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmve7t0jzg8

* fix(core): keep the hook context block on the turn that compacts

A suppressed UserPromptSubmit block is decided against pre-compaction
history, but automatic compaction replaces that history inside the same
send, so the block's only copy is dropped before the request is built
and the model sees it zero times on that turn. Pass the suppressed
payload down and re-append it when the send compacts.

Also restore the unconditional append in the ACP path when the prompt
resolved to no parts: `parts.length > 0` gated the append as well as the
suppression, so an empty-parts turn reached `toParts([])` and threw.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvec3c4pgi

* docs(design): draw the receipt-survival boundary at the operation

The design doc said the receipt survives "in-memory history churn" and drew
the boundary at serialization. `structuredClone` is an in-memory operation
that also drops symbol-keyed properties, and the prepared-request replay
installs history that way (`llm-chat.ts`), so the sentence taught the opposite
of the invariant it states: every in-memory path is safe.

Name the operation instead of the medium, and note that a replayed
prepared request re-injects once and re-arms, plus the same case in the Risks
list. Both language docs updated.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvegdno2gr

* fix(core,cli): keep a suppressed hook block through every compaction site

bdb593edb kept the block when the send compacted before the request was
built, but two compaction sites still drop it: the ACP/IDE send compacts
through `tryCompressChat` on its own path, and a reactive payload-too-large
retry compacts after the first attempt. Both replace the history entry that
held the block's only copy, so a suppressed block reaches the model zero
times on the turn that compacts instead of once.

Carry the ACP site's suppressed payload into the send options and re-append
it where each site compacts, next to the existing plan-exit notice handling.
Each fix is pinned by a regression test that fails without the re-append.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvfvtibojp

* style(core): collapse the hook-context re-append call to one line

Prettier's `Run Prettier` step in Lint & Static flagged
packages/core/src/core/llm-chat.ts: the `self.history.push(createUserContent([
hookContextPart]))` call fits within the print width, so prettier wants it on
a single line. No behavior change.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-conflict/jmvfwj8aojq

* fix(acp): forward a suppressed hook block to LlmChat's compaction rescue

The ACP lane computed `suppressedHookContext` and handed it to
`#sendMessageStreamWithAutoCompression`, but that method never forwarded it to
`chat.sendMessageStream` — all three send branches passed only `promptId`. Its
own re-append is gated on there being no full-turn model override, so when an
override is active, or when its pre-send compression returns NOOP and LlmChat
compacts later in the same send, the block's only copy is dropped by compaction
and never re-appended.

Forward the option on every send branch, and keep exactly one owner per send:
this method clears it once its own compaction consumed it, so LlmChat cannot
append a second byte-identical block.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvgu42rwlh

* test(core): pin the hook-context rescue gate and its wrapped-block shape

R5-9: add a negative case proving the COMPRESSED conjunct is the only thing
stopping the pre-send rescue from appending on a non-compacting suppressed
send. Dropping the conjunct turns this test red.

R5-8: strengthen the compaction rescue assertion from a loose includes('CTX')
substring check to exactly one part whose text equals the wrapped block, so an
unwrapped {text:'CTX'} part or a second copy both fail.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvhb9cysmc

* fix(core): stop the ACP hook-block rescue from consuming its own token

R7-1: when Session's own tryCompressChat reported COMPRESSED it appended
the suppressed UserPromptSubmit block to the outgoing message and then
cleared hookRescueContext, so the send forwarded no suppressedHookContext
to chat.sendMessageStream. A reactive compaction later in the same send
replaces history with no tail preservation, destroying the entry that
held the appended block, and both LlmChat rescue sites gate on
options?.suppressedHookContext -- now undefined -- so the model answered
that turn with zero copies of the standing hook instruction, against the
one-owner invariant stated three lines above the clear.

Forward the option unconditionally and make LlmChat's hard-tier rescue
idempotent with the module's own receipt predicate
(hasUserPromptSubmitContext), so appending in Session and still
forwarding cannot yield a second copy. The reactive site needs no guard:
composePostCompactHistory rebuilds history without the module-private
receipt symbol, so an already-present scan there can never match.

R7-2: the design doc's Risks and Acceptance sections described
compaction as "re-injects once, then re-arms", the opposite of the
in-send rescue this PR ships. Corrected in both the English and zh-CN
twins, naming compaction as rescued in-send and carrying the R5-12
refinement that the reactive fallback arm pushes a sole-part entry that
can never re-arm suppression.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvlnq2qysa

* fix(cli): resolve UserPromptSubmit hook context to one core module instance

Session.ts imported createUserPromptSubmitContextPart and
hasUserPromptSubmitContext through the core package.json subpath alias
'@qwen-code/qwen-code-core/userPromptSubmitContext'. esbuild resolves that
alias to packages/core/dist/src/hooks/user-prompt-submit-context.js, while
llm-chat.ts imports the same module relatively and resolves to
packages/core/src/hooks/user-prompt-submit-context.ts. Two module instances
of one source file therefore coexist in the bundle, each minting its own
userPromptSubmitContextReceipt Symbol. The idempotency guard in llm-chat.ts
(!hasUserPromptSubmitContext) can never observe the receipt stamped by the
ACP session, so a compacting turn appends the hook context block twice.

Import through '@qwen-code/qwen-code-core/hooks/user-prompt-submit-context.js'
instead. packages/cli/tsconfig.json maps '@qwen-code/qwen-code-core/*' to
'../core/src/*', so this resolves to the same physical module llm-chat.ts
uses: one instance, one Symbol, and the guard can fire. This matches the
existing deep-import form already used in this file for
core/hooks/hook-execution-context.js.

Receipt unforgeability is unchanged - it stays a module-private plain
Symbol, not Symbol.for.

Co-authored-by: Qwen-Coder <[email protected]>
Patrol-Run: qwen-pr-closeout/jmvmbaubot0

---------

Co-authored-by: qwen-code-autofix <qwen-code-autofix@localhost>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-10-25 22:50:26 +08:00
qqqys 52ae53b02a fix(hooks): gate headless file and skill prompts before expansion (#14356)
* fix(hooks): gate headless file and skill prompts before expansion

Headless ran a custom command's or skill's action before UserPromptSubmit,
so a blocking hook could not stop embedded shell commands or a skill's
tool and hook registration, and the hook saw the expanded template.

Run UserPromptSubmit on the raw input before a file command or skill
(stacked skills included) expands, once per submission, and hand the
verdict to that submission's first model request instead of running the
hook again on the expanded text. The verdict is a single-use token bound
to the client, session, prompt id and hook owner; any other use throws.
A refused command is not expanded and the request blocks through the
existing UserPromptSubmit block path, so output and cleanup are unchanged.

* test(core): reject a prompt admission after the session changes
2026-10-16 09:37:25 +00:00
qqqysandqwen-code-dev-bot d13ff87407 feat(hooks): apply PreToolUse updatedInput with full revalidation (#13442)
* feat(hooks): apply PreToolUse updatedInput with full revalidation

A PreToolUse hook's updatedInput now replaces the whole tool input in the
terminal and in ACP sessions. The hook runs once per call before input
validation and permission checks, so the replacement goes through the
media gate, schema validation, permission rules, plan and AUTO mode, the
approval prompt and the invocation guard, and is what the tool and
PostToolUse receive. A PreToolUse ask joins the normal approval prompt
and can no longer be auto-approved.

Invalid replacements stop the call in both PreToolUse and
PermissionRequest output, and a PermissionRequest replacement is rebuilt
and checked again, including AUTO classification, before its one-time
allow applies. Shell calls that a hook may still rewrite are no longer
run in parallel on early admission paths, and managed runtime and
fixed-policy calls refuse replacements instead of ignoring them.

* fix(hooks): keep PermissionRequest deny under a PreToolUse ask

A PreToolUse ask no longer skips the PermissionRequest hook, so the
hook's deny still applies. Its allow never stands in for the user's
confirmation: it can only have a replacement input revalidated, which
the user then confirms.

In ACP sessions, AUTO recovery state now follows the replacement's own
classification before its one-time approval is consumed, so approving
a replacement's fallback resets the right counters.

The scheduler releases the batch abort listener in finally, so a throw
while preparing a batch cannot leave it pinned to the signal. A Host
confinement test from main now builds invocations from their input,
as rebuilding a replacement requires.

* fix(cli): release a rejected PermissionRequest replacement invocation

In ACP, a replacement input built from a PermissionRequest hook was
handed to cleanup only after it passed validation. A later deny,
PLAN block, AUTO block or cancellation returned early and released
only the original invocation, leaving the replacement's prepared
execution pending.

Hand the replacement to cleanup as soon as it is built, and release
the original right away since it will not run.

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-10-08 13:15:54 +00:00
qqqysandqwen-code-dev-bot e767e223c5 fix(hooks): deliver pre-tool and ACP failure context to the model (#13006)
* fix(hooks): deliver pre-tool and ACP failure context to the model

PreToolUse additionalContext was dropped on the deny/ask/stop branches of
the hook helper and never attached to the tool result in core or ACP, and
ACP only logged PostToolUseFailure context on both the returned-error and
thrown-error paths.

Append the sanitized context to the owning call's functionResponse:
- core keeps it per call across an ask bounce and assembles it after
  PostToolBatch, before the final batch budget; cancelled calls drop it
- ACP appends Pre and failure context to model-facing parts only, keeping
  UI updates and error projections unchanged
- the added text is bounded per call by truncateToolOutputThreshold

* fix(hooks): keep hook context out of nested and App results

- exec: a nested call's result is a value for the script, so its hook
  context is handed to the parent exec and delivered with that result
  (core via the nested scheduler, ACP via the nested run result)
- ACP MCP App calls have no model consumer and never append context
- core rechecks each call's abort signal at final assembly, so a turn
  cancelled during PostToolBatch drops the stored context
- share the functionResponse text append with PostToolBatch context
- run the hook-context integration test in the no-AK PR lane

* fix(hooks): route nested failure context and pin review cases

- core: a code-mode nested call's PostToolUseFailure context goes to the
  parent exec result instead of the error handed to the script, matching
  ACP and nested PreToolUse context
- ACP: nested hook context keeps dispatch order under concurrent nested
  calls, so truncation does not depend on completion order
- docs: scope delivery to Code Mode nested calls, the turn-cancel
  exception, the per-surface limits, and transcript/detail visibility
- tests: sandbox-safe decision file, self-delimiting markers and a real
  value check in the integration test, ACP agent-exit handling, plus
  ACP cancel/truncation/nested-failure and core contentLength and
  nested-failure regressions

* fix(hooks): drop hook context when the turn is cancelled before recording

- core: after the final output budget (which can await persistence),
  redo the budget without the context of any call whose turn was aborted
- ACP: queued records keep their parts without hook context; if the turn
  is aborted before finalizeAndRecord records them (e.g. while notifying
  the client or persisting), finalize and record those parts instead
- tests: cancellation during the final budget (core) and while notifying
  a completed or denied result (ACP); the ACP integration agent gets its
  own QWEN_HOME and is awaited before teardown

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-30 06:50:21 +00:00
31f6047e9d fix(core): isolate declarative subagent hooks by invocation (#12949)
* fix(core): scope declarative agent hooks to their owning invocation

* test(core): sync agent mocks with hook execution frame

* fix(core): bind clear hooks to the new session owner

* fix(core): preserve caller hook identity for internal forks

---------

Co-authored-by: qqqys <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-09-29 07:21:32 +00:00
pratik wayase c8d30896a7 fix(hooks): key enabled state by name, not identity (#11965)
* fix(hooks): key enabled state by name, not identity

* fix: address review feedback for hook registry reload warnings and docs
2026-09-26 22:11:04 +00:00
a64d8decc1 fix(web-shell): preserve reference tags across reloads (#12404)
* fix(web-shell): preserve reference tags across reloads

* fix(acp): validate daemon input annotations at record, replay, and render

Address review findings on the user-message tag replay PR:

- Skip non-object annotation elements when recording (new
  readDaemonInputAnnotations reader), replaying (per-element filter),
  and rendering (null guard in splitComposerTagContentByAnnotations),
  so one malformed element can no longer poison a persisted session on
  every reload.
- Cap caller-supplied annotations at MAX_DAEMON_INPUT_ANNOTATIONS = 256,
  matching the sibling attachment-references bound.
- Export DAEMON_INPUT_ANNOTATIONS_META_KEY from bridgeTypes and route
  the recorder, replay, and live-echo read sites through it so the
  wire/persist key cannot drift silently.
- Restore the deferred /advisor branch's no-payload test pin and split
  its annotation assertions into a dedicated case.
- Document the annotations payload trigger in hooks.md and the
  intentional queued-strip/composer styling scope in both design docs.

* test(cli): make post-index-change hook cases deterministic across filesystem ticks (#12404)

The ungated-status control assumed `git status` always rewrites the
index on a dirty tree, firing the tree-shipped post-index-change hook.
The rewrite only happens when an entry is racily clean, which held only
because beforeEach's add and commit usually land in one filesystem tick;
a loaded runner straddles the tick, status writes nothing, and the
control fails. Force raciness by re-adding README.md with a future-dated
mtime (resetting the canary the re-add's own index write fires), and
share the fixture with the guarded case so its negative assertion is
exercised against a hook that would genuinely fire ungated.

---------

Co-authored-by: 钉萁 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-22 06:05:57 +00:00
qqqys bfa2f38c1a fix(core): load system-scope hooks under their own source (#12077)
* fix(core): load system-scope hooks under their own source

Hooks in the System and SystemDefaults settings files had no channel of
their own. They reached the registry only through the merged `hooks`
fallback, so they were labelled User (or Project), and when the user and
workspace scopes both had hooks, or the user scope had hooks in an
untrusted folder, the fallback was never read and the system hooks did
not load at all.

- Settings.getSystemHooks() merges SystemDefaults then System with the
  normal merge strategy (hook lists concatenate).
- resolveHookSettingsForConfig passes system, user and project hooks
  through as read; the merged `hooks` is used only when no per-scope
  hooks were supplied at all.
- Config gains systemHooks / getSystemHooks(); the legacy merged fallback
  in getUserHooks/getProjectHooks applies only when no scope was
  supplied, so a user-only hook is no longer registered a second time
  under the project source. Planner dedup already collapsed that copy,
  so User/Project execution counts do not change.
- HookRegistry registers System, User, Project, Extensions in that order.
- /hooks reloads the System and SystemDefaults files too.

* test(core): keep the legacy fallback case independent of the system getter

* test(core): pin that planner dedup keeps a doubly registered hook to one run

* fix(core): document the real execution order of system hooks

Registration order does not decide execution order: getHooksForEvent sorts an
event's hooks by source priority, which puts System after Project and User and
before Extensions. Correct the guide and the registry comment, pin the order
on getHooksForEvent, and tighten the reload paragraph of the merged Browsing
section.

* fix(cli): keep user hook edits when a system settings file is unreadable

Reloading the System and SystemDefaults files in the same atomic batch as
the user and workspace files meant a broken administrator file made every
/hooks reload fail, including for edits to the user's own hooks, and the user
usually cannot repair that file. Reload the user and workspace files first,
then the system files on their own: a failure there keeps the previous system
snapshot, still applies the other edits, and names the file in the error.

Also rename the source-priority test after what it asserts, the planned
order, not an executed one.
2026-09-17 12:26:00 +00:00
qqqys c7387d90f2 docs(hooks): merge the duplicated Browsing sections (#12071)
The hooks guide had two sections titled "Browsing your hooks" about 1,470
lines apart: one describing the read-only browser, one describing the reload
the menu performs. Read side by side, "It does not change anything" and
"opening the menu reloads hook definitions" contradict each other.

Keep the first section in place, move the reload paragraphs into it and state
the distinction: the browser never edits settings, but it does re-read them.
2026-09-17 08:07:45 +00:00
qqqys 3178c0c597 fix(core): count Stop-hook blocks across tool round trips (#12062)
A Stop hook that blocks forces a continuation. When that continuation
calls a tool, the caller re-enters through a fresh top-level send, and
the consecutive-block count, which rode the per-call send options, was
lost. A hook that blocked on every check could therefore walk past
stopHookBlockingCap indefinitely.

Keep the hook-forced flag, the count and the ordered reasons in one
bounded record per prompt id on the client. The record is retired when
the stop is allowed, the cap is hit, the user steers, a new interaction
starts on that id (user query, retry, cron, notification, teammate or
goal turn), or the send exits abnormally. Concurrent side questions keep
their own records, and a re-minted teammate prompt id starts fresh.

In non-interactive text mode the Stop hook messages and the cap warning
are written to stderr, sanitized; JSON and stream-JSON output are
unchanged. ACP production code is unchanged: its loop already counted
per prompt() call, and its queued-user-input reset is now pinned.

The former goal Stop-hook exemption asked for in #11673 no longer
applies: the built-in goal Stop hook was removed in #11900.
2026-09-17 04:47:37 +00:00
qqqysandqqqys a5ca52c9cf feat(cli): reload hook registry when /hooks opens (#11904)
* feat(cli): reload hook registry when /hooks opens

Hooks were captured once at startup, so a hook added, changed or removed
in a settings file had no effect until restart. Opening the /hooks menu now
re-reads the settings files, hands the resolved hook fields to Config via a
new setHooksFromSettings, and reloads the hook registry before the menu
renders.

The hook fields are resolved by one helper shared with loadCliConfig, so
the reload applies the startup rules: bare and safe mode load no hooks,
user hooks fall back to the merged hooks setting, and project hooks load
only in a trusted folder. All three Config fields are replaced together so
a stale legacy snapshot cannot come back through the getter fallback.
Settings are re-read without reloading the environment or consuming the
startup corruption markers. A failed reload is logged and the menu still
opens. The non-interactive /hooks list is unchanged.

* fix(cli): import the debug logger from its core module in hooksCommand

* fix(cli): reload hooks safely from session settings

* fix(cli): wrap hook reload notices for terminal menus

* fix(cli): keep the hook reload notice as two translatable sentences

The notice was one locale key whose English value carried a newline so the
menu would break the line. `check-i18n` requires the English value to match
its key byte for byte, so CI failed, and every translation encoded its own
line breaks, which made layout part of the message data.

Split it into two sentence-level keys and let the views place the break: Ink
renders one Text per sentence, and the OpenTUI dialog renders one text row per
line of its notice prop.

---------

Co-authored-by: qqqys <[email protected]>
2026-09-16 03:28:00 +00:00
qqqysandqqqys 06d77ad486 docs(goal): retire the Stop-hook era prose after the implementation was removed (#11922)
* docs(goal): retire the Stop-hook era prose after the implementation was removed

The first-generation Goal implementation was deleted in #11900, but a
design doc, an i18n key, two comments and a setting description still
describe it: the input-control design for a Goal loop that ran as a
blocking Stop hook, the `/goal` description string nothing reads any
more, an ACP control comment naming an in-memory store and a type that
no longer exist, a "judge's verdict" field comment, and the per-turn
tool-call cap saying Goal iterations are Stop-hook continuations.

* docs(web-shell): correct the Goal reason comment the workspace types copied from the bridge

* docs(goal): address remaining review accuracy gaps

---------

Co-authored-by: qqqys <[email protected]>
2026-09-15 12:55:53 +00:00
qqqys 61553c71cd feat(cli): full /hooks dialog in OpenTUI (#11903)
Replace the one-line OpenTUI /hooks panel with a read-only browser that
moves from events to matchers to hooks to one hook's details, like the
ink dialog. Events without matcher support skip the matcher step, and a
banner says when safe mode, bare mode or disableAllHooks turned hooks off.

The rows come from a new core buildHooksListing(config): the registry
entries with their real enabled state, then the hooks registered for the
current session, each flattened into display fields plus the original
config. Each row records whether it came from the registry or the session,
since a subagent's registry hooks also carry the source `session`.

The ACP workspace hooks status is now built from the same listing, keeping
only registry rows; its response is unchanged and pinned by a new test.

Adds a "Browsing your hooks" section to the hooks guide and the new
labels to the en, zh and zh-TW locales.
2026-09-15 03:31:12 +00:00
qqqys f7e48d5c4b fix(core): keep escaped whitespace at pipe-list entry edges (#11860)
A hook matcher that is a pipe-separated list is rebuilt from its trimmed
entries. Trimming removed whitespace the author had escaped and left its
backslash dangling, so `\.env\ |\.pem` was rebuilt as `\.env\|\.pem`,
where the pipe is literal: the matcher silently stopped matching
`/x/secrets.pem`, and `write_file\ |note.*` stopped matching
`notebook_edit`. The rebuilt expression still compiled, so nothing was
logged.

Trailing whitespace at an entry edge is now kept when an odd run of
backslashes precedes it. After an even run the backslashes escape each
other, so the whitespace is still padding and is trimmed. Leading
whitespace is always padding.

Part of #11610.
2026-09-14 15:51:49 +00:00
qqqys 6c04a19927 docs(hooks): complete the event catalog and correct stale claims (#11863)
Cover all 22 hook events in the event table, the matcher table and the
per-event details. Add PostToolBatch, UserPromptExpansion, PostCompact,
StopFailure and InstructionsLoaded to both tables, and add detail
sections for PostToolBatch, UserPromptExpansion, PermissionDenied and
InstructionsLoaded.

Correct statements that no longer match the code: async hook output is
not delivered, PostCompact output is ignored, StopFailure fires on
different surfaces for API errors and loop detection, plain-text stdout
becomes model context only for command hooks, and the Stop row no longer
mentions Claude. Add a troubleshooting checklist for hooks that do not
fire.

Part of #11610.
2026-09-14 15:51:44 +00:00
qqqys 85631a3d35 fix(core)!: let bash expand project directory variables in command hooks (#11864)
Command hooks had $GEMINI_PROJECT_DIR and $CLAUDE_PROJECT_DIR replaced
with a shell-escaped path before launch, for every shell. Inside the
double-quoted form Claude Code documents,
"$CLAUDE_PROJECT_DIR/.claude/hooks/check.sh", the inserted quotes became
part of the path, so the hook failed whenever the project path contained
a space (bash) or at all (PowerShell).

All three variables are already exported in the hook environment, and
bash expands them itself, so bash commands are now passed through
unchanged. cmd.exe never expands $VAR and PowerShell reads a bare $VAR
as undefined, so those shells keep the rewrite, which now also covers
$QWEN_PROJECT_DIR and stops at a word boundary.

The hooks guide quotes its example commands, explains how each shell
sees the variables, and adds a migration note.

BREAKING CHANGE: in bash hooks, $QWEN_PROJECT_DIR, $CLAUDE_PROJECT_DIR
and $GEMINI_PROJECT_DIR are no longer replaced in the command text. An
unquoted variable in a project path containing spaces now word-splits,
and '$VAR' inside single quotes no longer expands; double-quote the
variable instead. cmd and PowerShell hooks are unchanged except that
$QWEN_PROJECT_DIR is now rewritten too and longer names such as
$CLAUDE_PROJECT_DIRS are left alone.
2026-09-14 15:03:36 +00:00
qqqys e04f2ec5d4 fix(core): accept Claude Code tool names in tool hook matchers (#11826)
A tool hook matcher copied from a Claude Code config, such as "Bash" or
"Write|Edit", never matched the shell or write_file tools: matcher aliases
came only from the tool id, display name and legacy names, while permission
rules already accepted Claude Code's names.

Tool matcher targets now also include every name the permission rule
parser resolves to the same tool. Each name still identifies exactly one
tool; the permission meta-categories are not applied, so "Read" matches
read_file but not grep_search, and "Bash" does not match monitor.
coreTools / excludeTools resolution is unchanged.

Fixes #11823
2026-09-14 09:51:59 +00:00
qqqys 632773e018 refactor(core): apply one matcher rule to every hook event (#11619)
* refactor(core): apply one matcher rule to every hook event

Hook matchers meant different things per event and per registration
path. Notification, PreCompact, PostCompact and StopFailure accepted only
a single exact value, so `permission_prompt|idle_prompt` never matched,
and session-scoped hooks, including skill frontmatter hooks, anchored
their regex while settings hooks did not.

One shared matchesHookPattern now serves the planner and the session
hooks manager: an empty matcher, `*` or `.*` matches everything; the
matcher or any entry of a `|` list matches exactly (tool events also
compare aliases exactly); otherwise it is an unanchored regex on the
subject. Existing tool, subagent, command, session and file-path matcher
behaviour is unchanged.

Part of #11610

* fix(core): decide pipe-list matchers entry by entry

Review follow-up. A `|` list compared its entries exactly and then fell
back to the whole pattern as a regex, so `Write|*` became an invalid
regex that matched nothing, and a stray pipe such as `write_file|edit|`
became a regex that matched everything. Lists are now decided entry by
entry with the full rule; empty entries are ignored and never widen the
list, and a group spanning the pipe such as `a(b|c)` still falls back to
one regex.

The planner no longer keeps its own copy of the match-all rule. The hooks
and skills guides now say that skill matchers are unanchored, that an
omitted skill matcher matches every tool, how to anchor runtime ids and
exclusions, and that aliases only match exactly.

Part of #11610

* fix(core): decide pipe-list matcher entries without compiling them

A pipe-list entry now matches only as `*`, `.*`, or an exact subject or
alias. Entries are no longer compiled one by one, so an escaped pipe such
as `notes\|todo\.md` keeps its whole-pattern meaning instead of letting
the `todo\.md` fragment match any path, and a group that spans the pipe
no longer logs "Invalid regex" warnings for its unbalanced fragments.

The whole-pattern fall-through is compiled from the non-empty entries, so
a stray `|` is dropped rather than disabling the fall-through:
`read_(file|edit)|` matches `read_file`, while a list of only `|` still
matches nothing and a trailing `|` never becomes a match-all.

Pin that a skill hook entry without `matcher:` matches every tool.

* fix(core): trim pipe-list matcher entries and split only on unescaped pipes

Trim each list entry at the split so the exact comparison and the rebuilt
regular expression see the same entries: `read_.* | edit` now matches
`read_file` instead of compiling alternatives that demand literal spaces.

Split only on a `|` that is not preceded by a backslash, so an escaped pipe
never manufactures an empty entry or a dangling escape: `notes\|` matches
`x/notes|y`, `a\||b` matches `b`, and `foo\|bar` no longer matches `bar`.

Scope the documented stray-pipe guarantee to lists that do not start with
`^` or `(`; those matchers are compiled as written.

* fix(core): split matcher lists only at top-level pipes

The list reading of a matcher split on every pipe not preceded by a
backslash, so it cut through character classes and groups and then trimmed
spaces that belonged to the expression: `foo[ |]bar` was rebuilt as
`foo[|]bar` and stopped matching `foo bar`, and `a(b | c)` lost its spaces
the same way. A single preceding backslash also hid a real separator after
an escaped backslash, so `C:\\temp\\|D:\\data\\|` kept its trailing empty
alternative and matched everything.

Split with a scan that reads backslashes in pairs and ignores pipes inside
`[...]` and groups, so only top-level entries are trimmed, compared and
rebuilt, and `read_.* | edit` keeps reading as `read_.*|edit`. Tests pin
each shape and the `^` and `(` guards, and the hooks guide says which pipes
separate entries.
2026-09-12 21:23:40 +00:00
qqqysandqwen-code-dev-bot f49dda1b2f fix(core)!: read command hook timeout in seconds (#11615)
* fix(core)!: read command hook timeout in seconds with a 600s default

Command hooks passed `timeout` to the timer as milliseconds, while HTTP
and prompt hooks read it as seconds, the settings schema described
seconds, and the hooks guide documented both. A command hook written
with `"timeout": 10` was killed after 10ms and failed open.

Command hook `timeout` is now read in seconds and defaults to 600s. A
value of 1000 or more is still read as milliseconds so existing settings
keep their meaning, and a debug-log warning names each such hook once.
Timeout errors report seconds. HTTP, prompt and function hooks are
unchanged. The schema description, generated JSON schema and every
command hook example in the guide now use seconds.

BREAKING CHANGE: a command hook `timeout` below 1000 is now seconds
rather than milliseconds.

Part of #11610

* fix(core): keep hook timeouts authoritative and surface legacy values

Review follow-up.

- Hook execution requests on the message bus no longer carry the bus's
  60-second default deadline, so a hook is bounded by its own timeout
  instead of being abandoned while it keeps running. Explicit caller
  timeouts and other request types are unchanged.
- Command hooks on MessageDisplay, StopFailure and SessionDelete, which
  outlive the CLI, default to 60 seconds instead of 600.
- Settings warnings now name each command hook whose timeout is still
  read as milliseconds; the advice never suggests a seconds value that
  would itself be read as milliseconds.
- The hooks guide scopes "seconds" to command, HTTP and prompt hooks and
  notes async slot usage; shipped external-context examples use seconds.
- Tests pin the async registration and supervisor deadlines, and reset
  the warning deduplication between cases.

Part of #11610

* docs(core): explain long command hook timeouts and blocking hold time

A command hook timeout of 1000 or more is still read as legacy
milliseconds, so a value meant as a long timeout in seconds, such as 1800
for 30 minutes, ends up as 1.8 seconds. The startup warning now also says
how to keep the value that was meant as seconds, and the hooks guide
explains that long timeouts stay in milliseconds. The guide also notes that
a synchronous hook on an event that waits for its result holds the turn
until it exits or times out, 600 seconds by default.

* fix(core): bound signal-less hook requests and cancel tool hooks on abort

The message bus now waives its default deadline only for hook execution
requests that carry an abort signal. Requests without one cannot be
cancelled, so they keep the 60-second deadline instead of waiting on the
hook's own timeout.

PreToolUse, PostToolUse and non-interrupt PostToolUseFailure hooks now
receive the tool's abort signal, in the payload and as the request signal,
so an abort stops the hook and releases the wait. Interrupt reports keep no
signal because it is already aborted there. The UserPromptSubmit hook gets
the send signal, and a send aborted during or right after that hook ends as
an abort instead of recording or sending the prompt.

MessageDisplay deliveries pass the turn signal to the request, warn when the
bus reports a failed hook execution, and stay quiet when the turn was
cancelled.

A command hook timeout written as a numeric string is read like the number
it holds instead of falling back to the default.

The startup warning for legacy millisecond timeouts is removed from this
change and tracked in #11675; the hooks guide now explains how to find such
hooks. Async hook timeout messages report seconds, the remaining first-party
millisecond values in integration fixtures and design docs are rewritten in
seconds, and the async hooks bullet names the 60-second default on events
whose hooks outlive the process.

* fix(core): keep the 60 second command hook default and drop hook bus signal threading

Raising the command hook default to 600 seconds forced two rounds of
reconciliation with the 60 second and 15 second bus deadlines: first by
waiving the deadline, then by threading abort signals through the tool
and prompt hook call sites. Each round produced new regressions (payload
vs request signal, already-aborted failure reports, unmigrated arity
assertions, orphaned hook children, cancels recorded as hook errors). The
default change and the cancellation work move to a follow-up issue, and
this change keeps only the unit.

- Command hook timeouts stay in seconds with a 60 second default, the
  same duration as before; values of 1000 or more keep their legacy
  millisecond meaning, and numeric strings are still coerced. The
  separate default for hooks that outlive the process is gone because it
  now equals the default.
- MessageBus, tool hook triggers, the scheduler, the UserPromptSubmit
  request, the MessageDisplay dispatcher and their tests are back to main.
- An unusable timeout such as "30s" is named in the debug log with the
  default it was replaced by, like a legacy millisecond value.
- Timeout warnings are recorded as sent only once debug logging can write
  them, and are deduplicated per session.
- Async hook timeout messages and a real spawned hook with timeout 1 now
  pin the seconds conversion.

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-12 10:44:06 +00:00
qqqys 7340de4f37 fix(core): report real stop_hook_active on Stop hooks (#11613)
* fix(core): report real stop_hook_active on Stop hooks

Both places that fire the Stop hook, the core client and the ACP session,
hard-coded stop_hook_active to true. A Stop hook that follows the usual
pattern of blocking only while stop_hook_active is false therefore never
blocked, and no hook could tell its own continuation apart.

The field is now false on the first stop check and true only when the
agent is continuing because a Stop hook blocked the previous stop, which
is what SubagentStop already reported. The core client derives it from
the stop hook iteration count it already threads into the continuation
turn; the ACP loop uses its own iteration counter.

Part of #11610

* fix(core): keep stop_hook_active across tool calls in a hook-forced chain

Review follow-up. The core client derived stop_hook_active and the
consecutive-block count from send options that only the internal
recursion carried, so a hook-forced continuation that called a tool came
back through a fresh call with the state lost: a guard-style hook
re-blocked forever and the blocking cap never fired. The next-speaker and
steer recursions, which spread the options, kept stale state instead.

The client now keeps the chain per prompt id, written by both blocking
branches and cleared when a stop is allowed, the cap is reached, the user
steers, or a Goal turn starts. The ACP loop reports the field from its
own flag, set only for continuations carrying a Stop hook reason and
cleared for drained user input, guard continuations and superseded ones,
leaving the iteration counter to the cap and continuation ids.

Tests cover a tool round trip, a next-speaker continuation, the
goal-bound branch, the guard reset sequence and a drained user turn.

Part of #11610

* fix(core): key stop_hook_active by prompt and keep the block count per call

Replace the single client-held Stop-hook chain with a set of prompt ids
whose previous Stop check was blocked. stop_hook_active reads that set,
so it still survives a tool round trip, while a second prompt stopping
on the same client (a concurrent side question) can no longer evict or
overwrite another prompt's state. An id is cleared when its stop is
allowed, the cap is hit, steer input replaces the turn, the send exits
abnormally, or a new interaction starts on that id (retry and goal turns
reuse the prompt id).

The consecutive-block count goes back to riding the per-call
stopHookState, as on main, so the built-in /goal hook stays bounded by
its own iteration budget. Counting blocks across tool round trips is
tracked in #11673.

In ACP, user input drained before or after a Stop check now also
restarts the consecutive-block count, so an allow discarded by the drain
no longer lets a user-driven block count as consecutive.
2026-09-12 04:20:46 +00:00
qqqys 28df8b8a78 feat(core): add permission_mode, agent_id and prompt_id to every hook input (#11618)
* feat(core): add permission_mode, agent_id and prompt_id to every hook input

Only tool, subagent and session-start events carried permission_mode, and
agent_id only appeared on SubagentStart and SubagentStop. Scripts shared
across events could not branch on either.

The common hook input now includes permission_mode (the session approval
mode, unless the event reports its own), agent_id when the event fires
inside a subagent, and prompt_id when the model turn is known.
PostToolUse and PostToolUseFailure also report duration_ms, measured from
the start of tool execution in both the core scheduler and ACP sessions.

The three copies of the approval-mode to permission-mode mapping are
replaced by one shared helper.

Part of #11610

* fix(core): time hook duration_ms on the monotonic clock

duration_ms on PostToolUse and PostToolUseFailure was the difference of two
Date.now() readings, so a system clock step while a long tool ran could
report a negative or inflated duration to audit hooks. Both the core
scheduler and the ACP session now read performance.now() and round the
elapsed time. The hooks guide also lists SessionStart among the events that
report agent_type.
2026-09-11 17:09:44 +00:00
qqqys c541bdffa4 fix(core): route plain-text hook stdout to additionalContext for prompt and session events (#11612)
* fix(core): route plain-text hook stdout to additionalContext for prompt and session events

A command hook that prints plain text and exits 0 was turned into a
systemMessage, which the SessionStart, UserPromptSubmit and
UserPromptExpansion consumers never read and nothing displays. An
`echo "context"` hook on those events was therefore a silent no-op.

On those three events plain-text stdout now becomes
hookSpecificOutput.additionalContext, so it reaches the model and chains
into the next sequential hook like JSON context does. Other events keep
the systemMessage conversion. Only stdout is promoted: the stderr fallback
used when stdout is empty stays a system message, and non-zero exits keep
their existing handling.

Part of #11610

* fix(core): harden plain-text hook stdout promotion

Review follow-up. Only a JSON object (optionally double-encoded) is
structured hook output; bare JSON values such as `42` or `[1, 2]` are
plain text instead of being dropped, and exit code 2 with a bare value on
stderr now blocks. Output shaped like a JSON object that fails to parse is
kept out of model context, as in Claude Code, with a debug-log warning.
Promoted text has terminal escapes and control characters stripped per
line. The comments now state the real rationale for the event set and
the actual stderr behaviour, and the hooks guide row matches.

Part of #11610
2026-09-11 08:09:21 +00:00
cbd2cbadec fix(acp): Preserve submitted prompt provenance for auto recall (#11455)
* fix(acp): Preserve submitted prompt provenance for auto recall

Co-authored-by: Qwen-Coder <[email protected]>

* fix(acp): Omit submitted_prompt on channel-classified turns (#11455)

Channel turns are machine-relayed deliveries (loop jobs, webhook tasks,
adapter-synthesized events), so nothing on them crossed a submission
boundary. Since submitted_prompt presence alone gates Auto Recall's
outbound provider search, emitting it there ran retrieval on unattended
turns and could publish composed wrapper text as user provenance. Gate
emission on a positive user-submission condition instead.

Also reconcile the producer-set contract this PR changed: hooks.md, the
Auto Recall READMEs, the in-product hook description with its locale
mirrors, and the recording payload comment now state the actual rule,
and the new design doc records the channel exclusion plus the
strict-decoder rollout warning.

* fix(acp): Require explicit submission provenance for auto recall

Carry original Web Shell composer text separately through admission and queues; keep background dispatches and undeclared requests ineligible. Reconcile hook and integration contracts and add regression coverage.

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-10 15:17:51 +00:00
qqqysandqqqys f6ba502c9b fix(core): allow manual retry after auto mode blocks (#11025)
* fix(core): allow manual retry after auto mode blocks

* fix(core): preserve auto mode retry guarantees

---------

Co-authored-by: qqqys <[email protected]>
2026-09-05 02:33:07 +00:00
Shaojin Wenandqwen-code-dev-bot 1db18fefdb fix(hooks): close four trust-boundary holes in hook execution (#10427)
* fix(hooks): close four trust-boundary holes in hook execution

HTTP hooks never follow redirects (a 3xx is a non-blocking failure and
the target is never contacted). A workspace may narrow the HTTP-hook URL
whitelist but never replace one set at User, System, or SystemDefaults
scope. Qwen-internal secrets are never substituted into settings, hook
URLs/headers, or channel configuration, compared case-insensitively for
Windows, and the external-tool guard token joins the denylist; the CLI's
duplicate settings resolver is replaced by a core subpath export so the
serve fast path stays light. Project-level skill and subagent side
effects (allowedTools grants and frontmatter hooks) require a trusted
folder, mirroring the settings-file project-hooks gate.

Replaces #8396.

* fix(hooks): re-apply skill side effects when trust is granted mid-session

The Skill tool's "already loaded" dedup return sat above the new trust
gate, so a project skill first invoked in an untrusted folder kept its
allowedTools and hooks unapplied for the whole session even after the
folder was trusted. The gate is now re-evaluated on every invocation;
both grants dedup, so re-applying is idempotent.

Also keep an own `__proto__` variable in the sanitized child env (spread
and delete instead of an assignment loop), correct the guard-token note
in the denylist rationale (the ACP child never receives it), and pin the
settings-layer and serve fast-path secret refusal with tests.

* fix(hooks): give the project-skill trust gate a removal side

The gate on a project skill's side effects was enforced only when they were applied: once a repo-supplied skill had registered its frontmatter hooks and granted its allowedTools as session allow rules, a folder trust revoked mid-session (an IDE trust notification flips it live) left both active until the session restarted. Rather than tracking which skills applied what and unregistering on a trust event, the grants are marked trust-gated and the two consumers re-read folder trust at decision time: the hook event handler skips a trust-gated session hook at fire time while the folder is untrusted, and the permission manager leaves a trust-gated session allow rule out of every decision (and the effective-rules listing) while it is. Trust granted again restores both, symmetric with the mid-session grant the gate already honoured, and nothing changes on rounds that registered no project-skill hook.

The per-invocation re-application of side effects also turned two WARN lines into the normal steady state of a trusted skill — a dedup returning zero and a hookless skill — so both are debug-level now, reworded to say what the zero means.

* fix(permissions): let an ungated same-raw grant clear the kept entry's trust gate

The session-allow dedup kept the first entry's trustGated flag, so a
user-level grant of a raw the project skill had already granted would
inherit the repo grant's suspension when folder trust is revoked. The
dedup now keeps the wider grant: an ungated arrival clears the flag; a
gated re-arrival (skill reload) stays an idempotent skip and never
re-gates a rule the user holds.

Also pins the slash-command path's grant payload in the
SkillCommandLoader trusted-folder test to exactly { trustGated: true },
so shipping the grants ungated reddens the suite instead of passing on
call counts alone.

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-09-01 01:18:58 +00:00
jinyeandQwen-Coder 9c074b5651 fix(core): Preserve fire-and-forget hooks after exit (#10288)
* fix(core): preserve fire-and-forget hooks after exit

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (#10288)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (#10288)

Co-authored-by: Qwen-Coder <[email protected]>

* codex: address PR review feedback (#10288)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(core): make surviving hook status authoritative

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-08-29 10:47:07 +00:00
b34a08d16f fix(core): separate hook context from transcript display (#7948)
* fix(core): separate hook context from transcript display

* test(ci): gate desktop transcript projection

* revert: keep desktop CI scope unchanged

* test: cover transcript display fallbacks

* fix(transcript): address review feedback

* fix(transcript): reconcile post-merge provenance paths

* fix(webui): preserve legacy transcript concatenation

* test(transcript): cover projection consumers

* fix(transcript): consolidate hook context projection

* fix(transcript): support single-field display provenance

* fix(transcript): strip hook context with invalid metadata

* test(acp): cover empty replay display text

---------

Co-authored-by: destire-mio <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-08-08 02:03:20 +00:00
26352fcc6a feat(external-context): Add optional Mem0 memory writes (#8507)
* feat(external-context): Add optional Mem0 memory writes

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): Preserve confirmation content visibility

Render PreToolUse confirmation reasons literally and keep long confirmations accessible through the virtualized TUI. Add unit and interactive regression coverage for Mem0 write confirmations.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(external-context): Address memory write review findings

Align Hook and MCP argument handling, distinguish definitive Provider rejections from ambiguous outcomes, improve deployment diagnostics, and document the write-back trust boundary.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): Refine plain-text confirmations

Render URLs consistently, avoid persistent virtual viewport gaps, and document the literal-rendering and managed deployment boundaries.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(external-context): Support Auto Edit write confirmation

Co-authored-by: Qwen-Coder <[email protected]>

* fix(external-context): Harden write confirmations

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): Measure virtual row height directly

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): Preserve YOLO Hook confirmation content

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
2026-08-07 16:52:33 +00:00
Baby Blue Viperandbabyblueviper1 e3479a6251 docs: worked example for a PreToolUse HTTP hook backed by an external judgment service (#8202)
* docs: worked example for a PreToolUse HTTP hook backed by an external judgment service

The existing remote-security-check config example points at a service
that has to already exist, without showing what that service actually
looks like. Adds a minimal, stdlib-only, runnable adapter (invinoveritas
/review as the judgment source) implementing the exact contract PreToolUse
HTTP hooks expect -- verified live against the real production API, not
just written to look plausible: a genuinely destructive shell command
returns permissionDecision: "deny" with a real explanation, a benign one
returns "allow", and the adapter fails open on any judgment-service-side
error so an outage never blocks legitimate tool calls.

* docs: address review feedback -- disclose affiliation, note swap point clearly

* docs: fix timeout mismatch, add data-handling note, make backend URL configurable, log fail-open state

---------

Co-authored-by: babyblueviper1 <[email protected]>
2026-07-31 13:56:33 +00:00
cf547b6a3c feat(hooks): add SessionDelete event (#8059)
* feat(hooks): add SessionDelete event

* test(hooks): cover SessionDelete failure paths

* fix(hooks): standardize SessionDelete failure handling

* fix(hooks): include session id in SessionDelete hook failure logs (#8059)

* docs(hooks): note that transcript_path is empty over ACP for SessionDelete (#8059)

* refactor(cli): deduplicate SessionDelete hook dispatch (#8059)

* fix(cli): keep SessionDelete hook dispatch ACP-safe

* fix(cli): align SessionDelete short description with other references (#8059)

---------

Co-authored-by: 欢伯 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Code Bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
2026-07-31 13:05:44 +00:00
953c9d8177 feat(core): tag UserPromptSubmit hook context and record display provenance (#7956)
* feat(core): tag UserPromptSubmit hook context and record display provenance

UserPromptSubmit additionalContext was appended to the request as a bare
text part and persisted verbatim, so hook-injected text was
indistinguishable from user-authored text in the transcript, polluted
resumed sessions, telemetry, and auto-memory recall queries.

- Wrap injected context in a reserved
  <qwen:user-prompt-submit-context> tag (hook output already escapes
  angle brackets, so the tag cannot be forged from inside).
- Record the pre-injection user prompt as systemPayload.displayText plus
  the injected string as hookContext on the user record; the model-bound
  message stays verbatim for faithful resume replay.
- Use the pre-injection prompt text for telemetry prompt attributes and
  managed auto-memory recall.
- Resume projection prefers displayText, strips a trailing whole-part
  tagged block when no payload exists, and leaves legacy bare-injected
  records unchanged.
- Apply the same tag wrapping on the ACP session injection path, which
  already records the pre-injection prompt.

Closes #7940

Co-authored-by: Cursor <[email protected]>

* docs: note UPS promptText TDZ ordering and sole-part resume guard

Document the conflict-resolution constraint that promptText must be
declared before the injection assignment, and the sole-part read-path
guard that keeps a user-authored whole-tag message intact.

Co-authored-by: Cursor <[email protected]>

* test(cli): cover at_command resume with tagged UPS context

Confirm the at_command branch still prefers payload.userText when a
paired user record carries a trailing tagged hook-context part, and
falls back to the tag-stripping projection only when userText is absent.

Co-authored-by: Cursor <[email protected]>

* fix(core): address PR 7956 review findings and Goal recording spy

Omit the optional UserPromptRecordPayload third arg when no hook
injected, so Goal admission spies expecting two args stay exact and
CI client-goal.test.ts passes.

Project plain UserPromptSubmit-augmented records through
transcript-replay with the same displayText / trailing-tag strip
fallback as the TUI, covering ACP/export surfaces. Strengthen the
displayText preference fixture so it disagrees with the tag-strip
path, and use the named UserPromptRecordPayload type in resume.

Co-authored-by: Cursor <[email protected]>

* fix(acp-bridge): import UPS tag helper via Node-free package export

transcript-replay is inlined into the browser daemon/transcript SDK
bundle. Importing isUserPromptSubmitContextPartText from the core
package barrel pulled the whole Node-bound core graph into that
bundle and failed CI (esbuild Could not resolve "node:*") across
Test, web-shell E2E, and Real daemon E2E.

Export the pure helper as @qwen-code/qwen-code-core/userPromptSubmitContext
and import that path instead.

Co-authored-by: Cursor <[email protected]>

* fix(test): alias userPromptSubmitContext for Vitest source resolution

CLI and acp-bridge Vitest configs already map goalWire/transcriptRecords
to TypeScript sources; without the same alias the new package export
fails import analysis and breaks dozens of CLI suites.

Co-authored-by: Cursor <[email protected]>

* fix(acp-bridge): keep images when projecting displayText user records

Preferring UserPromptSubmit displayText previously returned early and
skipped projectMessageParts, dropping multimodal inlineData. Rebuild
parts so displayText replaces text while images keep their order.

Co-authored-by: Cursor <[email protected]>

* fix(core): drop unused hookContext and cover image-only displayText

UserPromptRecordPayload.hookContext had no read sites; keep displayText
only and recover injected text from the tagged message part. Also cover
the image-only !replaced append path and simplify the recording guard.

Co-authored-by: Cursor <[email protected]>

* test: cover remaining UserPromptSubmit provenance Suggestions

Share stripTrailingUserPromptSubmitContextPart between TUI resume and
ACP replay, assert ACP Session tags additionalContext, and lock
telemetry to the pre-injection prompt text.

Co-authored-by: Cursor <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
2026-07-30 11:45:23 +00:00
sundayand欢伯 aac663f28a feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes (#7968)
* feat(hooks): add security.allowPrivateNetworkHooks to bypass SSRF range checks for trusted scopes

HTTP hooks hard-block all private/link-local address ranges via ssrfGuard,
which makes them unusable in platform-managed environments where the hook
receiver is a first-party, VPC-internal endpoint (e.g. an internal API
gateway resolving to 172.16.0.0/12).

Add an opt-in setting, security.allowPrivateNetworkHooks (default false),
that skips the SSRF IP-range checks in urlValidator.isBlocked (literal IPs)
and validateResolvedHost (literal + post-DNS-resolution paths).

Security properties:
- Honored only from User/System/SystemDefaults scopes; the value is
  stripped from Workspace settings during the merge (with a startup
  warning), so a cloned repository can never self-grant the bypass.
- BLOCKED_HOSTS (169.254.169.254, metadata.google.internal, ...) remains
  blocked even when the flag is on.
- Default false keeps every code path byte-for-byte compatible with
  current behavior; bare/safe mode forces it off.

* fix(hooks): enforce metadata endpoint blocklist regardless of allowPrivateNetworkHooks

Address review findings on #7968: with the flag on, cloud metadata
endpoints were reachable through gaps in the relaxed checks.

- ssrfGuard: add METADATA_IPS (169.254.169.254, 100.100.100.200) and
  isMetadataAddress(), which normalizes IPv4-mapped IPv6 forms
  (::ffff:a9fe:a9fe, ::ffff:6464:64c8, ...) via the existing
  extractMappedIPv4/expandIPv6Groups helpers.
- urlValidator.isBlocked: BLOCKED_HOSTS matching and the literal-IP
  isMetadataAddress check now run unconditionally; only the general
  range check (isBlockedAddress) is relaxed by the flag.
- httpHookRunner.validateResolvedHost: no longer returns early with the
  flag on — DNS resolution still runs and resolved addresses are checked
  against isMetadataAddress, so a hostname resolving to a metadata
  endpoint is blocked. DNS failures still defer to fetch, as before.
- settings warning text now lists User/System/SystemDefaults, matching
  the schema and docs.
- docs: precise wording — the flag relaxes only range checks; metadata
  endpoints stay blocked in all serialized forms and after DNS resolution.

The flag now opens RFC1918/CGNAT/link-local ranges only; cloud metadata
endpoints (169.254.169.254, 100.100.100.200 in any form, plus the
BLOCKED_HOSTS hostnames) are unreachable in every configuration.

---------

Co-authored-by: 欢伯 <[email protected]>
2026-07-29 13:04:45 +00:00
jinyeandQwen-Coder 17408f1028 feat(hooks): Add submitted prompt provenance (#7762)
* feat(hooks): add submitted prompt provenance

Add an optional pre-expansion prompt sidecar for interactive UserQuery hooks while preserving legacy prompt behavior and fail-closed provenance handling across queues, retries, and continuations.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): harden submitted prompt provenance

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): tighten submitted prompt provenance

Co-authored-by: Qwen-Coder <[email protected]>

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-07-27 15:53:15 +00:00
34a3d46006 fix(core): fire StopFailure hook on loop detection early returns (#7592)
* fix(core): fire StopFailure hook on loop detection early returns (#7588)

When loop detection (always-on safety or heuristic) terminates a turn
early via `return turn`, the Stop hook code after the streaming loop
was never reached. StopFailure hooks were only fired from the CLI
layer for API errors, not from client.ts for loop detection.

Added `loop_detected` to StopFailureErrorType and fire the
StopFailure hook via MessageBus before each loop detection early
return, so cleanup/notification hooks run regardless of how the
turn ends.

All 284 client tests and 682 hook tests pass.

* fix(core): use direct hookSystem call for loop-detection StopFailure (#7588)

The MessageBus bridge has no StopFailure case, so the hook never
executed. Switch to config.getHookSystem()?.fireStopFailureEvent()
(matching the CLI's API-error path), make it fire-and-forget per the
StopFailure contract, drop the stale last_assistant_message that
carried the previous turn's text, deduplicate via a private helper,
update docs with loop_detected, regenerate the settings schema, and
add regression tests for both loop-detection paths.

* test(core): add negative-path test for StopFailure hook disable guard (#7592)

* test(core): add negative-path tests for StopFailure hook guards (#7592)

---------

Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Qwen Code Autofix <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-07-25 11:29:02 +00:00
218dec6a6f feat(hooks): add MessageDisplay hook for mid-turn streaming (#6489)
* feat(hooks): add MessageDisplay hook for mid-turn streaming

Fires repeatedly as the assistant reply streams, before Stop (which only fires once at the end of the turn). Fire-and-forget, cumulative text payload, debounced (~200ms) except for the unconditional final firing. Fires from the single streaming loop in client.ts shared by the terminal UI and ACP paths.

Fixes #6488

* fix(hooks): address MessageDisplay review feedback

- Chain fire-and-forget MessageDisplay requests per message_id instead of
  firing them fully unbounded, so a slow hook command can't pile up
  concurrent processes.
- Gate the final flush on non-empty displayed_text and !signal.aborted,
  matching the adjacent Stop hook's guard.
- Document why the final flush intentionally re-sends the last debounced
  text (is_final itself is new information).
- Simplify the debounce constant's JSDoc to drop the competitor comparison.
- Add tests for the mid-stream debounced flush and the rejected-request
  warn path.

* test(hooks): drain microtasks before asserting on chained MessageDisplay calls

fireMessageDisplayHook now chains per-message_id through a promise (see
previous commit), so the final flush's actual messageBus.request() call
lands a few microtask ticks after the generator itself finishes — the
mid-stream-flush test needs to let that chain settle before asserting.

* fix(hooks): flush MessageDisplay is_final on every for-await exit path

The three early `return turn` paths inside the streaming loop (always-on
loop-detection safety, heuristic loop detection, and the stream Error event)
exited before the final MessageDisplay flush, which only sat after the loop
ended normally. Hook scripts relying on is_final: true to know when to flush
never received it when a turn ended via loop detection or an API error.

Extracts the flush into a shared closure and calls it from all four exits
(the three early returns plus the normal fall-through), instead of only the
one at the bottom of the loop. Adds regression tests for all three previously
missed exits, plus the two guard-coverage tests requested in review (abort
suppresses the flush, a tool-call-only turn with no Content events does not
fire a vacuous empty-text event).

Addresses the outstanding critical review comment and the follow-up test
coverage suggestion on PR #6489.

* fix(hooks): fire MessageDisplay on the ACP surface, coalesce delivery, drain is_final before turn end

Addresses the three findings from the local verification report on #6489:

- ACP/qwen serve (Finding 1): the delivery logic now lives in a shared
  MessageDisplayDispatcher (packages/core), and Session.ts wires it into
  all four raw-stream loops (main prompt, Stop-hook continuation, cron
  tick, background notification) — these surfaces consume GeminiChat's
  stream directly and never enter GeminiClient.sendMessageStream, so
  they need their own fire sites. The daemon no longer advertises an
  event it never emits.

- Slow-hook backlog (Finding 2): the per-message promise chain is
  replaced by coalescing delivery — at most one in-flight request plus
  one pending payload per message; newer flushes overwrite the pending
  slot, which is lossless because displayed_text is cumulative, and
  is_final is sticky. A slow hook now sees fewer, newer payloads instead
  of an ever-growing queue of stale ones.

- Headless is_final drop (Finding 3): finish() resolves only once every
  enqueued payload has actually been delivered, and every exit out of
  the streaming loops awaits it (early returns, normal fall-through,
  and the enclosing finally for uncaught exceptions), so a short-lived
  -p process can no longer exit with the final payload still queued.
  As a consequence, is_final delivery now strictly precedes the Stop
  hook rather than racing it.

Also: the failure log line carries the message_id, finish() is
idempotent, the review-requested tests are added (mid-stream and final
firings share one message_id; isFinal as the sole flush reason), and
hooks.md gains a delivery-semantics contract covering coalescing, the
drain guarantee, no is_final on cancellation, provisional
displayed_text, and multiple messages per tool-using turn.

Co-Authored-By: Claude Fable 5 <[email protected]>

* fix(hooks): bound MessageDisplay drain wait, fix test gaps flagged in review

finish() now gives up waiting on drain after 5s (MESSAGE_DISPLAY_DRAIN_TIMEOUT_MS) instead of blocking turn teardown for up to the full 60s hook timeout, per the re-verification's S1 finding. Delivery keeps running in the background past the timeout; only the caller's wait is bounded.

Also: add the config.ts bridge test for MessageDisplay field extraction (S5), and add the missing MessageDisplay/InstructionsLoaded entries to acpAgent.test.ts's HookEventName mock (S6).

Co-Authored-By: Claude Sonnet 5 <[email protected]>

* fix(hooks): dispatch MessageDisplay is_final alongside stale deliveries, share one drain budget

Round-3 review findings on #6489:

- finish() no longer queues the is_final payload behind an in-flight
  mid-stream delivery: the pending slot's supersession argument applies to
  the in-flight slot too, so the final payload is dispatched immediately,
  alongside the stale delivery if one is still running. is_final is handed
  to the hook the moment the message ends — before Stop — on every surface,
  and can no longer be dropped by a short-lived process exiting with it
  still queued (Finding 1).
- The bounded drain wait is memoized: every finish() call (explicit,
  finally, or concurrent) shares one promise and one timer, so the teardown
  ceiling is MESSAGE_DISPLAY_DRAIN_TIMEOUT_MS itself, not a multiple of it
  (Finding 2).
- hooks.md delivery semantics rewritten to match the shipped behavior,
  including the headless orphaned-hook caveat and the unspecified completion
  order between an overlapped stale execution and the final one (Finding 3).
- The dispatcher mirrors its warnings to console.warn itself (stderr on
  headless/ACP, ink patchConsole in the TUI) in addition to the injected
  debug-file sink, so hitting the drain timeout is visible by default
  (Finding 4).
- A superseded mid-stream delivery that fails after the final was
  dispatched no longer warns; failures during streaming still do.
- New tests: finish() twice while delivery is in flight (the exact
  client.ts sequence), concurrent finish() calls sharing one budget,
  is_final overtaking a held mid-stream delivery, and drain resolving on
  the final delivery alone.

* refactor(core): consolidate MessageDisplay finish() calls, dedupe test spy setup

client.ts: wrap the turn.run() streaming loop in try/finally so messageDisplay.finish() fires once instead of at each of the three early-return sites plus the post-loop path -- matching the pattern the four raw-stream loops in Session.ts already use for the same dispatcher.

message-display-dispatcher.test.ts: centralize the console.warn spy setup/teardown in beforeEach/afterEach instead of five repeated per-test try/finally blocks.

No behavior change: full client.test.ts (246/246) and the message-display-buffer/dispatcher suites (24/24) pass unchanged.

* docs(hooks): clarify MessageDisplay cancellation timing (round-4 nit)

* test(hooks): cover the 3 untested MessageDisplay dispatch sites, fix cancellation doc wording

Adds MessageDisplay is_final coverage for the Stop-hook continuation loop, the in-session cron fire, and the background-notification loop, each with a normal-completion and an abort case. Adds three MessageDisplayDispatcher edge-case tests: a delivery settling just before the drain timeout, an abort arriving after a drain wait has already started, and addChunk called after abort but before finish(). Rewords the cancellation-timing doc bullet to state the actual criterion (abort signal state when finish() runs) rather than an approximation of it.

---------

Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
2026-07-11 13:24:32 +00:00
顾盼 14f02c132a fix(core): Match hook display-name matchers to tool ids (#6373)
* fix(core): match hook tool display names

* fix(core): tighten hook matcher aliases

* fix(core): align hook matcher aliases
2026-07-08 02:22:55 +00:00
DragonandClaude bcdb44c5d3 docs(hooks): document PreToolUse permissionDecision 'ask' behavior (#6411)
Co-authored-by: Claude <[email protected]>
2026-07-06 22:18:49 +00:00
Zqcand俊良 8daeb5b1f9 feat(core): add configurable auto-compact threshold and Stop hook context usage (#4025) (#5868)
* feat(core): add configurable auto-compact threshold and Stop hook context usage (#4025)

Add two features requested in issue #4025:

1. Configurable auto-compact threshold via settings.json
   - Add context.autoCompactThreshold setting (0-1, default 0.7)
   - Extend computeThresholds(window, pct?) to accept optional pct parameter
   - Wire all 4 call sites (chatCompressionService, geminiChat, contextCommand, useContextualTips)
   - Large windows (>110K) dominated by absolute branch, custom threshold mainly affects small windows

2. Stop hook stdin payload includes context usage data
   - Add ContextUsageData interface and buildContextUsage helper
   - Extend StopInput with context_usage, context_limit, input_tokens fields
   - Wire 3 callers (Session.ts, client.ts, config.ts)
   - Enables hook scripts to observe context usage and suggest compact strategies

* fix(test): add getAutoCompactThreshold mock to geminiChat.test.ts, add NaN guard to buildContextUsage

* fix(review): address round 4 findings — schema constraints, Partial<ContextUsageData>, buildContextUsage validation

* docs: add context.autoCompactThreshold and Stop hook context usage fields documentation

* fix(review): add contextWindowSize fallback, pct clamp, doc accuracy, threshold propagation test

* fix: correct warn value in contextCommand test comment

* test(chatCompressionService): fix misleading pct=1 test name and assertion

* fix(chatCompressionService): prevent negative warn threshold for low pct values

* docs(chatCompressionService): update JSDoc warn formula to include max(0, ...) floor

* test(chatCompressionService): add pct clamping tests and fix NaN handling

Add tests for out-of-range pct values (-0.5, 1.5, NaN) to verify
computeThresholds clamping behavior. Fix implementation to use
Number.isFinite() check so NaN falls back to DEFAULT_PCT instead
of propagating through Math.max(0, NaN) which yields NaN.

* test(config): add MCP Stop dispatch validation tests

Add tests for buildContextUsage runtime validation in MCP Stop dispatch path:
- Valid numeric inputs produce correct ContextUsageData
- Missing/undefined fields return undefined
- String values rejected by Number.isFinite validation
- Negative values return undefined

Also add Number.isFinite check for contextWindowSize in buildContextUsage
to properly validate MCP input types at runtime.

* fix(chatCompressionService): fix TypeScript type narrowing for pct parameter

Use explicit undefined check before Number.isFinite to properly narrow
the number | undefined type in the ternary expression.

---------

Co-authored-by: 俊良 <[email protected]>
2026-06-28 10:17:57 +00:00
Shile Zhang daec44a4f3 feat(hooks): pass original API call ID (toolCallId) to hook system (#4918)
- Add toolCallId field to PreToolUse/PostToolUse/PostToolUseFailure hook inputs
- Thread toolCallId from LLM API response (toolCall.id) through to hook subprocess stdin
- Fix message bus handler in config.ts to forward toolCallId (was silently dropped)
- toolCallId is optional — only included when the LLM provider supplies an ID

Signed-off-by: Shile Zhang <[email protected]>
2026-06-18 08:20:38 +00:00
DennisYu07 57282ebb7d feat(hooks): add prompt hook type with LLM evaluation support (#3388)
* implement prompt hook

* resolve comment

* resolve comment

* resolve comment

* resolve comment

* fix unit test
2026-05-15 17:13:05 +08:00
DennisYu07 1c529e4f0a feat(hooks): Add TodoCreated and TodoCompleted hooks for todo lifecycle events (#3378)
* add TaskCreated and TaskCompleted

* resolve comment

* resolve lint

* change merge logic from simple to or

* resolve lint error

* reslove commnent

* fix i18n key mismatch and malformed imports

* resolve comment
2026-05-15 15:51:01 +08:00
Reid 07475026f6 fix(cli): remember "Start new chat session" until summary changes (#3308)
* fix(cli): remember "Start new chat session" until summary changes

  Persist a project-scoped Welcome Back restart choice keyed to the
  current PROJECT_SUMMARY fingerprint.

  This suppresses the Welcome Back dialog after choosing "Start new chat
  session", while still showing it again after the project summary is
  updated.

* fix conflict
2026-04-16 13:54:14 +08:00
DennisYu07andQwen-Coder b5115e731e feat(hooks): Add HTTP Hook, Function Hook and Async Hook support (#2827)
* add http/async/function type

* fix url error

* resolve comment

* align cc non blocking error

* fix hookRunner for async

* fix(hooks): update hook type validation to support http and function types

- Change validated hook types from ['command', 'plugin'] to ['command', 'http', 'function']
- Add validation for HTTP hooks requiring url field
- Add validation for function hooks requiring callback field
- Add comprehensive test coverage for all hook type validations

Co-authored-by: Qwen-Coder <[email protected]>

* fix(hooks): align SSRF protection with Claude Code behavior

- Allow 127.0.0.0/8 (loopback) for local dev hooks
- Allow localhost hostname for local dev hooks
- Allow ::1 (IPv6 loopback) for local dev hooks
- Add 100.64.0.0/10 (CGNAT) to blocked ranges (RFC 6598)
- Update tests to match Claude Code's ssrfGuard.ts behavior

This fixes HTTP hooks failing to connect to local dev servers.

Co-authored-by: Qwen-Coder <[email protected]>

* refactor(hooks): align HTTP hook security with Claude Code behavior

- Add CRLF/NUL sanitization for env var interpolation (header injection)
- Implement combined abort signal (external signal + timeout)
- Upgrade SSRF protection to DNS-level with ssrfGuard
  - Allow loopback (127.0.0.0/8, ::1) for local dev hooks
  - Block CGNAT (100.64.0.0/10) and IPv6 private ranges
- Increase default HTTP hook timeout to 10 minutes
- Fix VS Code hooks schema to support http type
  - Add url, headers, allowedEnvVars, async, once, statusMessage, shell fields
  - Note: "function" type is SDK-only (callback cannot be serialized to JSON)

* feat(hooks): enhance Function Hook with messages, skillRoot, shell, and matcher support

- Add MessagesProvider for automatic conversation history passing to function hooks
- Add FunctionHookContext with messages, toolUseID, and signal
- Add skillRoot support for skill-scoped session hooks
- Add shell parameter support for command hooks (bash/powershell)
- Add regex matcher support for hook pattern matching
- Add statusMessage to CommandHookConfig
- Change default function hook timeout from 60s to 5s
- Add comprehensive unit tests for all new features

Co-authored-by: Qwen-Coder <[email protected]>

* add session hook for skill

* fix function hook parsing

* refactor ui for http hook/async hook/function hook

* update doc and add integration test

* change telemetryn type and refactor SSRF

* fix project level bug

---------

Co-authored-by: Qwen-Coder <[email protected]>
2026-04-16 10:10:33 +08:00
DennisYu07 dddb56d885 feat: add stopFailure and postCompact (#2825) 2026-04-13 12:54:44 +08:00
DennisYu07 5221002831 remove hooks experimental and refactor hook Config 2026-04-01 11:50:23 +08:00