feat(server): in-app project hooks — live config, fail-closed, in-app approvals - #1

Merged
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation
Aug 13, 2026
Merged

feat(server): in-app project hooks — live config, fail-closed, in-app approvals#1
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation

Conversation

@Defmon3

@Defmon3Defmon3 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Adds a Claude-hooks-style PreToolUse gate for Full access threads, configured per project via .t3code/hooks.json, with approvals surfaced in the app — then solidifies it:

  • Live config: .t3code/hooks.json is re-read before every hook check. Claude threads pick up create/edit/delete on the next tool call; Codex threads apply command/matcher edits on the next approval and recompute approval routing each turn.
  • Fail closed: an unreadable or invalid config turns tool calls into approval prompts (with a logged warning) instead of silently allowing.
  • No silent drops: unsupported hook event keys (PostToolUse, Stop, …) warn once in the server log.
  • Hook decisions: allow / ask / deny via stdout JSON or exit codes; Claude-compatible hookSpecificOutput accepted. Codex coverage maps command approvals → Bash, file changes → Edit.

Verification

  • vp test run over the 4 touched test files: 127/127 pass
  • vp run --filter t3 typecheck: clean in changed files
  • Two independent gpt-5.6-sol reviews over the rebased frozen scope; one low-severity warning-key collision was fixed and narrowly re-reviewed to zero surviving findings
  • Execution plan checked in at .plans/solidify-project-hooks.md; user docs updated in docs/user/permission-modes.md

🤖 Generated with Claude Code

Summary by Sourcery

Make T3 project hooks a live, fail-closed config for full-access sessions and align Codex approval routing with hook presence.

New Features:

  • Add live re-reading of .t3code/hooks.json before each PreToolUse hook evaluation so mid-session config changes take effect without restarting.
  • Expose a hasPreToolUseHooksNow signal on T3HookPlan for providers to detect current hook presence per turn.
  • Surface hook config failures as user-facing approval prompts instead of silently allowing tool calls.

Enhancements:

  • Warn once per config file about unsupported hook event keys while still running supported PreToolUse hooks.
  • Refine Codex full-access approval routing to stop requesting callbacks when a turn has no active project hooks.
  • Document project hook live-reload behavior, supported events, and unreadable-config handling in permission-modes user docs.
  • Add an internal execution plan document capturing the design and scope of the project-hooks solidification work.

Tests:

  • Extend T3HookRunner tests to cover live config creation/editing, fail-closed behavior on invalid configs, and unsupported-event warnings.
  • Adjust Codex and Claude adapter tests to account for the new hasPreToolUseHooksNow field and Codex approval routing behavior.
  • Add CodexSessionRuntime tests verifying approvalPolicy mapping when interceptApprovals is disabled in full-access mode.

@sourcery-ai

sourcery-aiBot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements live, fail-closed project hook behavior for PreToolUse across Claude and Codex runtimes, adds unsupported-event warnings, wires Codex approval routing to live hook presence, and documents the new behavior in user docs and an execution plan.

Sequence diagram for live PreToolUse evaluation and fail-closed behavior

sequenceDiagram
actor User
participant ClaudeAdapter
participant T3HookRunner
participant FileSystem
participant HookCommand
User->>ClaudeAdapter: invoke full-access tool
ClaudeAdapter->>T3HookRunner: prepare(cwd)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
T3HookRunner->>FileSystem: findConfigPath + read hooks.json
FileSystem-->>T3HookRunner: hooks.json content
T3HookRunner->>T3HookRunner: decodeHooksConfigJson
T3HookRunner->>T3HookRunner: decodeHooksConfigEventKeysJson
T3HookRunner->>T3HookRunner: logWarning on unsupported events
T3HookRunner-->>ClaudeAdapter: T3HookPlan(hasPreToolUseHooks, hasPreToolUseHooksNow)
loop each tool call
ClaudeAdapter->>T3HookRunner: plan.evaluatePreToolUse(input)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
alt hooks.json unreadable/invalid
T3HookRunner->>T3HookRunner: logConfigFailure(T3HookConfigError)
T3HookRunner-->>ClaudeAdapter: decision ask, title "T3 hook config failed"
else hooks.json valid
T3HookRunner->>HookCommand: run hook command
HookCommand-->>T3HookRunner: HookCommandOutput (allow/ask/deny)
T3HookRunner-->>ClaudeAdapter: normalizedDecision
end
end
ClaudeAdapter-->>User: tool allowed / approval prompt / denied
Loading

File-Level Changes

ChangeDetailsFiles
Make T3HookRunner re-resolve hooks config on every evaluation and expose live hook presence and fail-closed behavior.
  • Introduce HooksConfigEventKeys schema and SUPPORTED_HOOK_EVENTS to parse hook event keys from hooks.json.
  • Add warning deduping for unsupported hook events and emit Effect.logWarning with path and event details.
  • Add hasPreToolUseHooksNow effect to T3HookPlan and implement resolvePlanState helper to re-read config each time.
  • Update prepare to take an initial snapshot but have evaluatePreToolUse re-resolve config, handling T3HookConfigError by logging and returning an ask decision.
  • Add logConfigFailure helper to centralize config failure logging and reuse it in live evaluation paths.
apps/server/src/hooks/T3HookRunner.ts
Extend T3HookRunner tests to cover live config creation and edits, fail-closed behavior on invalid config, and unsupported-event warnings.
  • Add writeHooksConfig helper to create .t3code/hooks.json in a temp project directory.
  • Test that a config created after prepare toggles hasPreToolUseHooksNow to true and affects evaluation decisions.
  • Test that matcher edits are picked up without re-preparing, allowing previously matched calls when hooks no longer match.
  • Test that invalid JSON in hooks.json causes hasPreToolUseHooksNow to report hooks and evaluation to return an ask decision with a failure title and reason.
  • Test that configs declaring unsupported events (PostToolUse, Stop) log exactly one warning while still running PreToolUse hooks.
apps/server/src/hooks/T3HookRunner.test.ts
Wire CodexSessionRuntime to use spawn-time hook snapshot for initial thread open but recompute approval routing per turn from live hook presence.
  • Rename initial interceptApprovals flag to interceptApprovalsAtStart and keep using it only for openCodexThread.
  • On each sendTurn, derive interceptApprovals from options.hookPlan.hasPreToolUseHooksNow to reflect live config changes.
  • Ensure buildTurnStartParams receives the recomputed interceptApprovals for each turn.
  • Add a test that when a full-access turn has no T3 hooks, approvalPolicy is set to never while sandboxPolicy stays dangerFullAccess.
apps/server/src/provider/Layers/CodexSessionRuntime.ts
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Adjust Claude and Codex adapters and their tests to satisfy the expanded T3HookPlan interface with hasPreToolUseHooksNow.
  • Update ClaudeAdapter getHookPlan fallback plan to set hasPreToolUseHooksNow to Effect.succeed(false) when no hook runner is present.
  • Update ClaudeAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan stubs.
  • Update CodexAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan literals checked by identity.
  • Leave existing approval flows and evaluation behavior unchanged apart from the new field.
apps/server/src/provider/Layers/ClaudeAdapter.ts
apps/server/src/provider/Layers/ClaudeAdapter.test.ts
apps/server/src/provider/Layers/CodexAdapter.test.ts
Document live hook config behavior, supported events, and unreadable-config handling, and check in the execution plan for solidifying project hooks.
  • Extend permission-modes documentation with details on live hooks.json re-reads for Claude and Codex threads.
  • Describe that only PreToolUse is supported and that other events are logged once as warnings, not executed.
  • Explain fail-closed behavior when hooks.json becomes unreadable, turning tool calls into approval prompts.
  • Add .plans/solidify-project-hooks.md capturing the locked execution plan and rationale for the implementation.
docs/user/permission-modes.md
.plans/solidify-project-hooks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 13, 2026

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In readConfig, you decode the same JSON string twice (once via decodeHooksConfigJson and again via decodeHooksConfigEventKeysJson just to get event keys); consider deriving unsupportedEvents directly from config.hooks to avoid the extra parse and reduce complexity.
  • hasPreToolUseHooksNow currently uses a broad Effect.catch that logs a config failure and returns true for any error; narrowing this to T3HookConfigError (similar to the evaluatePreToolUse catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `readConfig`, you decode the same JSON string twice (once via `decodeHooksConfigJson` and again via `decodeHooksConfigEventKeysJson` just to get event keys); consider deriving `unsupportedEvents` directly from `config.hooks` to avoid the extra parse and reduce complexity.
-`hasPreToolUseHooksNow` currently uses a broad `Effect.catch` that logs a config failure and returns `true` for any error; narrowing this to `T3HookConfigError` (similar to the `evaluatePreToolUse` catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
## Individual Comments### Comment 1
<locationpath="apps/server/src/hooks/T3HookRunner.ts"line_range="384" />
<code_context>
return { decision: "allow" } satisfies T3HookDecision;
});
+ const resolvePlanState = Effect.fn("T3HookRunner.resolvePlanState")(function* (cwd: string) {
+ const configPathOption = yield* findConfigPath(cwd);
+ if (Option.isNone(configPathOption)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new hook plan flow by caching path-derived state, centralizing config-error handling, and deriving unsupported event keys from the already-decoded config instead of extra schemas.
- The extra indirection around config/state resolution and error handling is noticeable. You can keep the new live behaviors but simplify the flow and reduce repeated work.
### 1. Avoid re-running `findConfigPath` on every operation
You currently do:
```tsconst snapshot =yield*resolvePlanState(cwd);
// ...hasPreToolUseHooksNow: resolvePlanState(cwd).pipe(/* ... */),
evaluatePreToolUse: (input) =>Effect.gen(function* () {
const state =yield*resolvePlanState(cwd);
// ...
}).pipe(/* ... */),
````resolvePlanState` re-runs `findConfigPath(cwd)` every time, even though `cwd` is fixed per `prepare` call. You can keep live config re-reads but cache the `configPath` and `projectDirectory` once and re-use them:
```tsconst prepare:T3HookRunner["Service"]["prepare"] =Effect.fn("T3HookRunner.prepare")(function* (cwd) {
const configPathOption =yield*findConfigPath(cwd);
if (Option.isNone(configPathOption)) {
return {
configPath: undefined,
hasPreToolUseHooks: false,
hasPreToolUseHooksNow: Effect.succeed(false),
evaluatePreToolUse: () =>Effect.succeed({ decision: "allow"asconst }),
} satisfiesT3HookPlan;
}
const configPath =configPathOption.value;
const projectDirectory =path.dirname(path.dirname(configPath));
const readCurrentState =Effect.gen(function* () {
const config =yield*readConfig(configPath);
return {
configPath,
projectDirectory,
entries: config.hooks.PreToolUse?? ([] asReadonlyArray<HookMatcherConfig>),
};
});
const initialState =yield*readCurrentState;
const hasHooks =initialState.entries.length>0;
return {
configPath,
hasPreToolUseHooks: hasHooks,
hasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>logConfigFailure(error).pipe(Effect.as(true))),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
Effect.flatMap((state) =>state.entries.length===0?Effect.succeed({ decision: "allow" } satisfiesT3HookDecision)
:evaluateEntries({
entries: state.entries,
configPath: state.configPath,
projectDirectory: state.projectDirectory!,
payload: { ...input, cwd },
}),
),
Effect.catchTag(
"T3HookConfigError",
handleConfigFailure, // see helper below
),
),
} satisfiesT3HookPlan;
});
```
This keeps:
- Single `findConfigPath` per `prepare`.
- Live config re-reading via `readCurrentState`.
- Snapshot fields (`configPath`, `hasPreToolUseHooks`) plus dynamic ones, but with clearer boundaries.
### 2. Centralize error → decision mapping
Right now the mapping from `T3HookConfigError` to `T3HookDecision` is inlined inside `prepare`:
```tsEffect.catchTag("T3HookConfigError", (error) =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
),
),
```
Encapsulating this makes both `hasPreToolUseHooksNow` and `evaluatePreToolUse` easier to read:
```tsconst handleConfigFailure = (error:T3HookConfigError):Effect.Effect<T3HookDecision> =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
);
```
Then:
```tshasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>errorinstanceofT3HookConfigError?handleConfigFailure(error).pipe(Effect.map(() =>true))
:logConfigFailure(errorasT3HookConfigError).pipe(Effect.as(true)),
),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
// ...Effect.catchTag("T3HookConfigError", handleConfigFailure),
),
```
This keeps your UX behavior identical but removes duplicated decision construction and shortens the pipelines.
### 3. Simplify unsupported-event warning
You added a second schema + decoder just to get event keys:
```tsconst HooksConfigEventKeys =Schema.Struct({
hooks: Schema.Record(Schema.String, Schema.Unknown),
});
const decodeHooksConfigEventKeysJson =Schema.decodeUnknownEffect(
fromLenientJson(HooksConfigEventKeys),
);
const declaredEvents =yield*decodeHooksConfigEventKeysJson(raw).pipe(
Effect.map((decoded) =>Object.keys(decoded.hooks)),
Effect.orElseSucceed(() => [] asReadonlyArray<string>),
);
```
Since you already successfully decoded `config` above, you can derive event keys directly from it and drop the extra schema/decoder:
```tsconst config =yield*decodeHooksConfigJson(raw);
// ...const declaredEvents =Object.keys(config.hooks);
const unsupportedEvents =declaredEvents
.filter((event) =>!SUPPORTED_HOOK_EVENTS.includes(eventas (typeofSUPPORTED_HOOK_EVENTS)[number]))
.sort();
if (unsupportedEvents.length>0) {
if (!warnedUnsupportedEvents.has(configPath)) {
warnedUnsupportedEvents.add(configPath);
yield*Effect.logWarning("ignoring unsupported T3 hook events", {
path: configPath,
unsupportedEvents,
supportedEvents: SUPPORTED_HOOK_EVENTS,
});
}
}
```
If “warn once per config file” is sufficient, keying `warnedUnsupportedEvents` by `configPath` alone removes the need to build and track `warningKey` strings.
These changes keep all current behaviors (live config updates, fail-closed, warnings) but reduce indirection, duplicated logic, and statefulness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadapps/server/src/hooks/T3HookRunner.ts
Defmon3and others added 3 commits August 13, 2026 14:56
Re-resolve .t3code/hooks.json on every PreToolUse evaluation so mid-session
edits, creation, and deletion take effect without a restart; recompute Codex
approval routing per turn from the live config; fail closed (ask) with a
logged warning when the config becomes unreadable; warn once per config on
unsupported hook event keys instead of silently ignoring them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Defmon3
Defmon3force-pushed the feat/in-app-hook-confirmation branch from 6e304cb to 83d9fd8CompareAugust 13, 2026 13:05
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 83d9fd8.

This comment will update automatically after the next completed run.

@Defmon3
Defmon3 merged commit 75288e8 into customAug 13, 2026
8 of 12 checks passed
@Defmon3
Defmon3 deleted the feat/in-app-hook-confirmation branch August 13, 2026 13:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Defmon3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(server): in-app project hooks — live config, fail-closed, in-app approvals - #1

Merged
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation
Aug 13, 2026
Merged

feat(server): in-app project hooks — live config, fail-closed, in-app approvals#1
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation

Conversation

@Defmon3

@Defmon3Defmon3 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Adds a Claude-hooks-style PreToolUse gate for Full access threads, configured per project via .t3code/hooks.json, with approvals surfaced in the app — then solidifies it:

  • Live config: .t3code/hooks.json is re-read before every hook check. Claude threads pick up create/edit/delete on the next tool call; Codex threads apply command/matcher edits on the next approval and recompute approval routing each turn.
  • Fail closed: an unreadable or invalid config turns tool calls into approval prompts (with a logged warning) instead of silently allowing.
  • No silent drops: unsupported hook event keys (PostToolUse, Stop, …) warn once in the server log.
  • Hook decisions: allow / ask / deny via stdout JSON or exit codes; Claude-compatible hookSpecificOutput accepted. Codex coverage maps command approvals → Bash, file changes → Edit.

Verification

  • vp test run over the 4 touched test files: 127/127 pass
  • vp run --filter t3 typecheck: clean in changed files
  • Two independent gpt-5.6-sol reviews over the rebased frozen scope; one low-severity warning-key collision was fixed and narrowly re-reviewed to zero surviving findings
  • Execution plan checked in at .plans/solidify-project-hooks.md; user docs updated in docs/user/permission-modes.md

🤖 Generated with Claude Code

Summary by Sourcery

Make T3 project hooks a live, fail-closed config for full-access sessions and align Codex approval routing with hook presence.

New Features:

  • Add live re-reading of .t3code/hooks.json before each PreToolUse hook evaluation so mid-session config changes take effect without restarting.
  • Expose a hasPreToolUseHooksNow signal on T3HookPlan for providers to detect current hook presence per turn.
  • Surface hook config failures as user-facing approval prompts instead of silently allowing tool calls.

Enhancements:

  • Warn once per config file about unsupported hook event keys while still running supported PreToolUse hooks.
  • Refine Codex full-access approval routing to stop requesting callbacks when a turn has no active project hooks.
  • Document project hook live-reload behavior, supported events, and unreadable-config handling in permission-modes user docs.
  • Add an internal execution plan document capturing the design and scope of the project-hooks solidification work.

Tests:

  • Extend T3HookRunner tests to cover live config creation/editing, fail-closed behavior on invalid configs, and unsupported-event warnings.
  • Adjust Codex and Claude adapter tests to account for the new hasPreToolUseHooksNow field and Codex approval routing behavior.
  • Add CodexSessionRuntime tests verifying approvalPolicy mapping when interceptApprovals is disabled in full-access mode.

@sourcery-ai

sourcery-aiBot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements live, fail-closed project hook behavior for PreToolUse across Claude and Codex runtimes, adds unsupported-event warnings, wires Codex approval routing to live hook presence, and documents the new behavior in user docs and an execution plan.

Sequence diagram for live PreToolUse evaluation and fail-closed behavior

sequenceDiagram
actor User
participant ClaudeAdapter
participant T3HookRunner
participant FileSystem
participant HookCommand
User->>ClaudeAdapter: invoke full-access tool
ClaudeAdapter->>T3HookRunner: prepare(cwd)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
T3HookRunner->>FileSystem: findConfigPath + read hooks.json
FileSystem-->>T3HookRunner: hooks.json content
T3HookRunner->>T3HookRunner: decodeHooksConfigJson
T3HookRunner->>T3HookRunner: decodeHooksConfigEventKeysJson
T3HookRunner->>T3HookRunner: logWarning on unsupported events
T3HookRunner-->>ClaudeAdapter: T3HookPlan(hasPreToolUseHooks, hasPreToolUseHooksNow)
loop each tool call
ClaudeAdapter->>T3HookRunner: plan.evaluatePreToolUse(input)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
alt hooks.json unreadable/invalid
T3HookRunner->>T3HookRunner: logConfigFailure(T3HookConfigError)
T3HookRunner-->>ClaudeAdapter: decision ask, title "T3 hook config failed"
else hooks.json valid
T3HookRunner->>HookCommand: run hook command
HookCommand-->>T3HookRunner: HookCommandOutput (allow/ask/deny)
T3HookRunner-->>ClaudeAdapter: normalizedDecision
end
end
ClaudeAdapter-->>User: tool allowed / approval prompt / denied
Loading

File-Level Changes

ChangeDetailsFiles
Make T3HookRunner re-resolve hooks config on every evaluation and expose live hook presence and fail-closed behavior.
  • Introduce HooksConfigEventKeys schema and SUPPORTED_HOOK_EVENTS to parse hook event keys from hooks.json.
  • Add warning deduping for unsupported hook events and emit Effect.logWarning with path and event details.
  • Add hasPreToolUseHooksNow effect to T3HookPlan and implement resolvePlanState helper to re-read config each time.
  • Update prepare to take an initial snapshot but have evaluatePreToolUse re-resolve config, handling T3HookConfigError by logging and returning an ask decision.
  • Add logConfigFailure helper to centralize config failure logging and reuse it in live evaluation paths.
apps/server/src/hooks/T3HookRunner.ts
Extend T3HookRunner tests to cover live config creation and edits, fail-closed behavior on invalid config, and unsupported-event warnings.
  • Add writeHooksConfig helper to create .t3code/hooks.json in a temp project directory.
  • Test that a config created after prepare toggles hasPreToolUseHooksNow to true and affects evaluation decisions.
  • Test that matcher edits are picked up without re-preparing, allowing previously matched calls when hooks no longer match.
  • Test that invalid JSON in hooks.json causes hasPreToolUseHooksNow to report hooks and evaluation to return an ask decision with a failure title and reason.
  • Test that configs declaring unsupported events (PostToolUse, Stop) log exactly one warning while still running PreToolUse hooks.
apps/server/src/hooks/T3HookRunner.test.ts
Wire CodexSessionRuntime to use spawn-time hook snapshot for initial thread open but recompute approval routing per turn from live hook presence.
  • Rename initial interceptApprovals flag to interceptApprovalsAtStart and keep using it only for openCodexThread.
  • On each sendTurn, derive interceptApprovals from options.hookPlan.hasPreToolUseHooksNow to reflect live config changes.
  • Ensure buildTurnStartParams receives the recomputed interceptApprovals for each turn.
  • Add a test that when a full-access turn has no T3 hooks, approvalPolicy is set to never while sandboxPolicy stays dangerFullAccess.
apps/server/src/provider/Layers/CodexSessionRuntime.ts
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Adjust Claude and Codex adapters and their tests to satisfy the expanded T3HookPlan interface with hasPreToolUseHooksNow.
  • Update ClaudeAdapter getHookPlan fallback plan to set hasPreToolUseHooksNow to Effect.succeed(false) when no hook runner is present.
  • Update ClaudeAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan stubs.
  • Update CodexAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan literals checked by identity.
  • Leave existing approval flows and evaluation behavior unchanged apart from the new field.
apps/server/src/provider/Layers/ClaudeAdapter.ts
apps/server/src/provider/Layers/ClaudeAdapter.test.ts
apps/server/src/provider/Layers/CodexAdapter.test.ts
Document live hook config behavior, supported events, and unreadable-config handling, and check in the execution plan for solidifying project hooks.
  • Extend permission-modes documentation with details on live hooks.json re-reads for Claude and Codex threads.
  • Describe that only PreToolUse is supported and that other events are logged once as warnings, not executed.
  • Explain fail-closed behavior when hooks.json becomes unreadable, turning tool calls into approval prompts.
  • Add .plans/solidify-project-hooks.md capturing the locked execution plan and rationale for the implementation.
docs/user/permission-modes.md
.plans/solidify-project-hooks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 13, 2026

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In readConfig, you decode the same JSON string twice (once via decodeHooksConfigJson and again via decodeHooksConfigEventKeysJson just to get event keys); consider deriving unsupportedEvents directly from config.hooks to avoid the extra parse and reduce complexity.
  • hasPreToolUseHooksNow currently uses a broad Effect.catch that logs a config failure and returns true for any error; narrowing this to T3HookConfigError (similar to the evaluatePreToolUse catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `readConfig`, you decode the same JSON string twice (once via `decodeHooksConfigJson` and again via `decodeHooksConfigEventKeysJson` just to get event keys); consider deriving `unsupportedEvents` directly from `config.hooks` to avoid the extra parse and reduce complexity.
-`hasPreToolUseHooksNow` currently uses a broad `Effect.catch` that logs a config failure and returns `true` for any error; narrowing this to `T3HookConfigError` (similar to the `evaluatePreToolUse` catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
## Individual Comments### Comment 1
<locationpath="apps/server/src/hooks/T3HookRunner.ts"line_range="384" />
<code_context>
return { decision: "allow" } satisfies T3HookDecision;
});
+ const resolvePlanState = Effect.fn("T3HookRunner.resolvePlanState")(function* (cwd: string) {
+ const configPathOption = yield* findConfigPath(cwd);
+ if (Option.isNone(configPathOption)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new hook plan flow by caching path-derived state, centralizing config-error handling, and deriving unsupported event keys from the already-decoded config instead of extra schemas.
- The extra indirection around config/state resolution and error handling is noticeable. You can keep the new live behaviors but simplify the flow and reduce repeated work.
### 1. Avoid re-running `findConfigPath` on every operation
You currently do:
```tsconst snapshot =yield*resolvePlanState(cwd);
// ...hasPreToolUseHooksNow: resolvePlanState(cwd).pipe(/* ... */),
evaluatePreToolUse: (input) =>Effect.gen(function* () {
const state =yield*resolvePlanState(cwd);
// ...
}).pipe(/* ... */),
````resolvePlanState` re-runs `findConfigPath(cwd)` every time, even though `cwd` is fixed per `prepare` call. You can keep live config re-reads but cache the `configPath` and `projectDirectory` once and re-use them:
```tsconst prepare:T3HookRunner["Service"]["prepare"] =Effect.fn("T3HookRunner.prepare")(function* (cwd) {
const configPathOption =yield*findConfigPath(cwd);
if (Option.isNone(configPathOption)) {
return {
configPath: undefined,
hasPreToolUseHooks: false,
hasPreToolUseHooksNow: Effect.succeed(false),
evaluatePreToolUse: () =>Effect.succeed({ decision: "allow"asconst }),
} satisfiesT3HookPlan;
}
const configPath =configPathOption.value;
const projectDirectory =path.dirname(path.dirname(configPath));
const readCurrentState =Effect.gen(function* () {
const config =yield*readConfig(configPath);
return {
configPath,
projectDirectory,
entries: config.hooks.PreToolUse?? ([] asReadonlyArray<HookMatcherConfig>),
};
});
const initialState =yield*readCurrentState;
const hasHooks =initialState.entries.length>0;
return {
configPath,
hasPreToolUseHooks: hasHooks,
hasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>logConfigFailure(error).pipe(Effect.as(true))),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
Effect.flatMap((state) =>state.entries.length===0?Effect.succeed({ decision: "allow" } satisfiesT3HookDecision)
:evaluateEntries({
entries: state.entries,
configPath: state.configPath,
projectDirectory: state.projectDirectory!,
payload: { ...input, cwd },
}),
),
Effect.catchTag(
"T3HookConfigError",
handleConfigFailure, // see helper below
),
),
} satisfiesT3HookPlan;
});
```
This keeps:
- Single `findConfigPath` per `prepare`.
- Live config re-reading via `readCurrentState`.
- Snapshot fields (`configPath`, `hasPreToolUseHooks`) plus dynamic ones, but with clearer boundaries.
### 2. Centralize error → decision mapping
Right now the mapping from `T3HookConfigError` to `T3HookDecision` is inlined inside `prepare`:
```tsEffect.catchTag("T3HookConfigError", (error) =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
),
),
```
Encapsulating this makes both `hasPreToolUseHooksNow` and `evaluatePreToolUse` easier to read:
```tsconst handleConfigFailure = (error:T3HookConfigError):Effect.Effect<T3HookDecision> =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
);
```
Then:
```tshasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>errorinstanceofT3HookConfigError?handleConfigFailure(error).pipe(Effect.map(() =>true))
:logConfigFailure(errorasT3HookConfigError).pipe(Effect.as(true)),
),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
// ...Effect.catchTag("T3HookConfigError", handleConfigFailure),
),
```
This keeps your UX behavior identical but removes duplicated decision construction and shortens the pipelines.
### 3. Simplify unsupported-event warning
You added a second schema + decoder just to get event keys:
```tsconst HooksConfigEventKeys =Schema.Struct({
hooks: Schema.Record(Schema.String, Schema.Unknown),
});
const decodeHooksConfigEventKeysJson =Schema.decodeUnknownEffect(
fromLenientJson(HooksConfigEventKeys),
);
const declaredEvents =yield*decodeHooksConfigEventKeysJson(raw).pipe(
Effect.map((decoded) =>Object.keys(decoded.hooks)),
Effect.orElseSucceed(() => [] asReadonlyArray<string>),
);
```
Since you already successfully decoded `config` above, you can derive event keys directly from it and drop the extra schema/decoder:
```tsconst config =yield*decodeHooksConfigJson(raw);
// ...const declaredEvents =Object.keys(config.hooks);
const unsupportedEvents =declaredEvents
.filter((event) =>!SUPPORTED_HOOK_EVENTS.includes(eventas (typeofSUPPORTED_HOOK_EVENTS)[number]))
.sort();
if (unsupportedEvents.length>0) {
if (!warnedUnsupportedEvents.has(configPath)) {
warnedUnsupportedEvents.add(configPath);
yield*Effect.logWarning("ignoring unsupported T3 hook events", {
path: configPath,
unsupportedEvents,
supportedEvents: SUPPORTED_HOOK_EVENTS,
});
}
}
```
If “warn once per config file” is sufficient, keying `warnedUnsupportedEvents` by `configPath` alone removes the need to build and track `warningKey` strings.
These changes keep all current behaviors (live config updates, fail-closed, warnings) but reduce indirection, duplicated logic, and statefulness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadapps/server/src/hooks/T3HookRunner.ts
Defmon3and others added 3 commits August 13, 2026 14:56
Re-resolve .t3code/hooks.json on every PreToolUse evaluation so mid-session
edits, creation, and deletion take effect without a restart; recompute Codex
approval routing per turn from the live config; fail closed (ask) with a
logged warning when the config becomes unreadable; warn once per config on
unsupported hook event keys instead of silently ignoring them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Defmon3
Defmon3force-pushed the feat/in-app-hook-confirmation branch from 6e304cb to 83d9fd8CompareAugust 13, 2026 13:05
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 83d9fd8.

This comment will update automatically after the next completed run.

@Defmon3
Defmon3 merged commit 75288e8 into customAug 13, 2026
8 of 12 checks passed
@Defmon3
Defmon3 deleted the feat/in-app-hook-confirmation branch August 13, 2026 13:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Defmon3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(server): in-app project hooks — live config, fail-closed, in-app approvals - #1

Merged
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation
Aug 13, 2026
Merged

feat(server): in-app project hooks — live config, fail-closed, in-app approvals#1
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation

Conversation

@Defmon3

@Defmon3Defmon3 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Adds a Claude-hooks-style PreToolUse gate for Full access threads, configured per project via .t3code/hooks.json, with approvals surfaced in the app — then solidifies it:

  • Live config: .t3code/hooks.json is re-read before every hook check. Claude threads pick up create/edit/delete on the next tool call; Codex threads apply command/matcher edits on the next approval and recompute approval routing each turn.
  • Fail closed: an unreadable or invalid config turns tool calls into approval prompts (with a logged warning) instead of silently allowing.
  • No silent drops: unsupported hook event keys (PostToolUse, Stop, …) warn once in the server log.
  • Hook decisions: allow / ask / deny via stdout JSON or exit codes; Claude-compatible hookSpecificOutput accepted. Codex coverage maps command approvals → Bash, file changes → Edit.

Verification

  • vp test run over the 4 touched test files: 127/127 pass
  • vp run --filter t3 typecheck: clean in changed files
  • Two independent gpt-5.6-sol reviews over the rebased frozen scope; one low-severity warning-key collision was fixed and narrowly re-reviewed to zero surviving findings
  • Execution plan checked in at .plans/solidify-project-hooks.md; user docs updated in docs/user/permission-modes.md

🤖 Generated with Claude Code

Summary by Sourcery

Make T3 project hooks a live, fail-closed config for full-access sessions and align Codex approval routing with hook presence.

New Features:

  • Add live re-reading of .t3code/hooks.json before each PreToolUse hook evaluation so mid-session config changes take effect without restarting.
  • Expose a hasPreToolUseHooksNow signal on T3HookPlan for providers to detect current hook presence per turn.
  • Surface hook config failures as user-facing approval prompts instead of silently allowing tool calls.

Enhancements:

  • Warn once per config file about unsupported hook event keys while still running supported PreToolUse hooks.
  • Refine Codex full-access approval routing to stop requesting callbacks when a turn has no active project hooks.
  • Document project hook live-reload behavior, supported events, and unreadable-config handling in permission-modes user docs.
  • Add an internal execution plan document capturing the design and scope of the project-hooks solidification work.

Tests:

  • Extend T3HookRunner tests to cover live config creation/editing, fail-closed behavior on invalid configs, and unsupported-event warnings.
  • Adjust Codex and Claude adapter tests to account for the new hasPreToolUseHooksNow field and Codex approval routing behavior.
  • Add CodexSessionRuntime tests verifying approvalPolicy mapping when interceptApprovals is disabled in full-access mode.

@sourcery-ai

sourcery-aiBot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements live, fail-closed project hook behavior for PreToolUse across Claude and Codex runtimes, adds unsupported-event warnings, wires Codex approval routing to live hook presence, and documents the new behavior in user docs and an execution plan.

Sequence diagram for live PreToolUse evaluation and fail-closed behavior

sequenceDiagram
actor User
participant ClaudeAdapter
participant T3HookRunner
participant FileSystem
participant HookCommand
User->>ClaudeAdapter: invoke full-access tool
ClaudeAdapter->>T3HookRunner: prepare(cwd)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
T3HookRunner->>FileSystem: findConfigPath + read hooks.json
FileSystem-->>T3HookRunner: hooks.json content
T3HookRunner->>T3HookRunner: decodeHooksConfigJson
T3HookRunner->>T3HookRunner: decodeHooksConfigEventKeysJson
T3HookRunner->>T3HookRunner: logWarning on unsupported events
T3HookRunner-->>ClaudeAdapter: T3HookPlan(hasPreToolUseHooks, hasPreToolUseHooksNow)
loop each tool call
ClaudeAdapter->>T3HookRunner: plan.evaluatePreToolUse(input)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
alt hooks.json unreadable/invalid
T3HookRunner->>T3HookRunner: logConfigFailure(T3HookConfigError)
T3HookRunner-->>ClaudeAdapter: decision ask, title "T3 hook config failed"
else hooks.json valid
T3HookRunner->>HookCommand: run hook command
HookCommand-->>T3HookRunner: HookCommandOutput (allow/ask/deny)
T3HookRunner-->>ClaudeAdapter: normalizedDecision
end
end
ClaudeAdapter-->>User: tool allowed / approval prompt / denied
Loading

File-Level Changes

ChangeDetailsFiles
Make T3HookRunner re-resolve hooks config on every evaluation and expose live hook presence and fail-closed behavior.
  • Introduce HooksConfigEventKeys schema and SUPPORTED_HOOK_EVENTS to parse hook event keys from hooks.json.
  • Add warning deduping for unsupported hook events and emit Effect.logWarning with path and event details.
  • Add hasPreToolUseHooksNow effect to T3HookPlan and implement resolvePlanState helper to re-read config each time.
  • Update prepare to take an initial snapshot but have evaluatePreToolUse re-resolve config, handling T3HookConfigError by logging and returning an ask decision.
  • Add logConfigFailure helper to centralize config failure logging and reuse it in live evaluation paths.
apps/server/src/hooks/T3HookRunner.ts
Extend T3HookRunner tests to cover live config creation and edits, fail-closed behavior on invalid config, and unsupported-event warnings.
  • Add writeHooksConfig helper to create .t3code/hooks.json in a temp project directory.
  • Test that a config created after prepare toggles hasPreToolUseHooksNow to true and affects evaluation decisions.
  • Test that matcher edits are picked up without re-preparing, allowing previously matched calls when hooks no longer match.
  • Test that invalid JSON in hooks.json causes hasPreToolUseHooksNow to report hooks and evaluation to return an ask decision with a failure title and reason.
  • Test that configs declaring unsupported events (PostToolUse, Stop) log exactly one warning while still running PreToolUse hooks.
apps/server/src/hooks/T3HookRunner.test.ts
Wire CodexSessionRuntime to use spawn-time hook snapshot for initial thread open but recompute approval routing per turn from live hook presence.
  • Rename initial interceptApprovals flag to interceptApprovalsAtStart and keep using it only for openCodexThread.
  • On each sendTurn, derive interceptApprovals from options.hookPlan.hasPreToolUseHooksNow to reflect live config changes.
  • Ensure buildTurnStartParams receives the recomputed interceptApprovals for each turn.
  • Add a test that when a full-access turn has no T3 hooks, approvalPolicy is set to never while sandboxPolicy stays dangerFullAccess.
apps/server/src/provider/Layers/CodexSessionRuntime.ts
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Adjust Claude and Codex adapters and their tests to satisfy the expanded T3HookPlan interface with hasPreToolUseHooksNow.
  • Update ClaudeAdapter getHookPlan fallback plan to set hasPreToolUseHooksNow to Effect.succeed(false) when no hook runner is present.
  • Update ClaudeAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan stubs.
  • Update CodexAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan literals checked by identity.
  • Leave existing approval flows and evaluation behavior unchanged apart from the new field.
apps/server/src/provider/Layers/ClaudeAdapter.ts
apps/server/src/provider/Layers/ClaudeAdapter.test.ts
apps/server/src/provider/Layers/CodexAdapter.test.ts
Document live hook config behavior, supported events, and unreadable-config handling, and check in the execution plan for solidifying project hooks.
  • Extend permission-modes documentation with details on live hooks.json re-reads for Claude and Codex threads.
  • Describe that only PreToolUse is supported and that other events are logged once as warnings, not executed.
  • Explain fail-closed behavior when hooks.json becomes unreadable, turning tool calls into approval prompts.
  • Add .plans/solidify-project-hooks.md capturing the locked execution plan and rationale for the implementation.
docs/user/permission-modes.md
.plans/solidify-project-hooks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 13, 2026

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In readConfig, you decode the same JSON string twice (once via decodeHooksConfigJson and again via decodeHooksConfigEventKeysJson just to get event keys); consider deriving unsupportedEvents directly from config.hooks to avoid the extra parse and reduce complexity.
  • hasPreToolUseHooksNow currently uses a broad Effect.catch that logs a config failure and returns true for any error; narrowing this to T3HookConfigError (similar to the evaluatePreToolUse catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `readConfig`, you decode the same JSON string twice (once via `decodeHooksConfigJson` and again via `decodeHooksConfigEventKeysJson` just to get event keys); consider deriving `unsupportedEvents` directly from `config.hooks` to avoid the extra parse and reduce complexity.
-`hasPreToolUseHooksNow` currently uses a broad `Effect.catch` that logs a config failure and returns `true` for any error; narrowing this to `T3HookConfigError` (similar to the `evaluatePreToolUse` catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
## Individual Comments### Comment 1
<locationpath="apps/server/src/hooks/T3HookRunner.ts"line_range="384" />
<code_context>
return { decision: "allow" } satisfies T3HookDecision;
});
+ const resolvePlanState = Effect.fn("T3HookRunner.resolvePlanState")(function* (cwd: string) {
+ const configPathOption = yield* findConfigPath(cwd);
+ if (Option.isNone(configPathOption)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new hook plan flow by caching path-derived state, centralizing config-error handling, and deriving unsupported event keys from the already-decoded config instead of extra schemas.
- The extra indirection around config/state resolution and error handling is noticeable. You can keep the new live behaviors but simplify the flow and reduce repeated work.
### 1. Avoid re-running `findConfigPath` on every operation
You currently do:
```tsconst snapshot =yield*resolvePlanState(cwd);
// ...hasPreToolUseHooksNow: resolvePlanState(cwd).pipe(/* ... */),
evaluatePreToolUse: (input) =>Effect.gen(function* () {
const state =yield*resolvePlanState(cwd);
// ...
}).pipe(/* ... */),
````resolvePlanState` re-runs `findConfigPath(cwd)` every time, even though `cwd` is fixed per `prepare` call. You can keep live config re-reads but cache the `configPath` and `projectDirectory` once and re-use them:
```tsconst prepare:T3HookRunner["Service"]["prepare"] =Effect.fn("T3HookRunner.prepare")(function* (cwd) {
const configPathOption =yield*findConfigPath(cwd);
if (Option.isNone(configPathOption)) {
return {
configPath: undefined,
hasPreToolUseHooks: false,
hasPreToolUseHooksNow: Effect.succeed(false),
evaluatePreToolUse: () =>Effect.succeed({ decision: "allow"asconst }),
} satisfiesT3HookPlan;
}
const configPath =configPathOption.value;
const projectDirectory =path.dirname(path.dirname(configPath));
const readCurrentState =Effect.gen(function* () {
const config =yield*readConfig(configPath);
return {
configPath,
projectDirectory,
entries: config.hooks.PreToolUse?? ([] asReadonlyArray<HookMatcherConfig>),
};
});
const initialState =yield*readCurrentState;
const hasHooks =initialState.entries.length>0;
return {
configPath,
hasPreToolUseHooks: hasHooks,
hasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>logConfigFailure(error).pipe(Effect.as(true))),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
Effect.flatMap((state) =>state.entries.length===0?Effect.succeed({ decision: "allow" } satisfiesT3HookDecision)
:evaluateEntries({
entries: state.entries,
configPath: state.configPath,
projectDirectory: state.projectDirectory!,
payload: { ...input, cwd },
}),
),
Effect.catchTag(
"T3HookConfigError",
handleConfigFailure, // see helper below
),
),
} satisfiesT3HookPlan;
});
```
This keeps:
- Single `findConfigPath` per `prepare`.
- Live config re-reading via `readCurrentState`.
- Snapshot fields (`configPath`, `hasPreToolUseHooks`) plus dynamic ones, but with clearer boundaries.
### 2. Centralize error → decision mapping
Right now the mapping from `T3HookConfigError` to `T3HookDecision` is inlined inside `prepare`:
```tsEffect.catchTag("T3HookConfigError", (error) =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
),
),
```
Encapsulating this makes both `hasPreToolUseHooksNow` and `evaluatePreToolUse` easier to read:
```tsconst handleConfigFailure = (error:T3HookConfigError):Effect.Effect<T3HookDecision> =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
);
```
Then:
```tshasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>errorinstanceofT3HookConfigError?handleConfigFailure(error).pipe(Effect.map(() =>true))
:logConfigFailure(errorasT3HookConfigError).pipe(Effect.as(true)),
),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
// ...Effect.catchTag("T3HookConfigError", handleConfigFailure),
),
```
This keeps your UX behavior identical but removes duplicated decision construction and shortens the pipelines.
### 3. Simplify unsupported-event warning
You added a second schema + decoder just to get event keys:
```tsconst HooksConfigEventKeys =Schema.Struct({
hooks: Schema.Record(Schema.String, Schema.Unknown),
});
const decodeHooksConfigEventKeysJson =Schema.decodeUnknownEffect(
fromLenientJson(HooksConfigEventKeys),
);
const declaredEvents =yield*decodeHooksConfigEventKeysJson(raw).pipe(
Effect.map((decoded) =>Object.keys(decoded.hooks)),
Effect.orElseSucceed(() => [] asReadonlyArray<string>),
);
```
Since you already successfully decoded `config` above, you can derive event keys directly from it and drop the extra schema/decoder:
```tsconst config =yield*decodeHooksConfigJson(raw);
// ...const declaredEvents =Object.keys(config.hooks);
const unsupportedEvents =declaredEvents
.filter((event) =>!SUPPORTED_HOOK_EVENTS.includes(eventas (typeofSUPPORTED_HOOK_EVENTS)[number]))
.sort();
if (unsupportedEvents.length>0) {
if (!warnedUnsupportedEvents.has(configPath)) {
warnedUnsupportedEvents.add(configPath);
yield*Effect.logWarning("ignoring unsupported T3 hook events", {
path: configPath,
unsupportedEvents,
supportedEvents: SUPPORTED_HOOK_EVENTS,
});
}
}
```
If “warn once per config file” is sufficient, keying `warnedUnsupportedEvents` by `configPath` alone removes the need to build and track `warningKey` strings.
These changes keep all current behaviors (live config updates, fail-closed, warnings) but reduce indirection, duplicated logic, and statefulness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadapps/server/src/hooks/T3HookRunner.ts
Defmon3and others added 3 commits August 13, 2026 14:56
Re-resolve .t3code/hooks.json on every PreToolUse evaluation so mid-session
edits, creation, and deletion take effect without a restart; recompute Codex
approval routing per turn from the live config; fail closed (ask) with a
logged warning when the config becomes unreadable; warn once per config on
unsupported hook event keys instead of silently ignoring them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Defmon3
Defmon3force-pushed the feat/in-app-hook-confirmation branch from 6e304cb to 83d9fd8CompareAugust 13, 2026 13:05
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 83d9fd8.

This comment will update automatically after the next completed run.

@Defmon3
Defmon3 merged commit 75288e8 into customAug 13, 2026
8 of 12 checks passed
@Defmon3
Defmon3 deleted the feat/in-app-hook-confirmation branch August 13, 2026 13:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Defmon3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(server): in-app project hooks — live config, fail-closed, in-app approvals - #1

Merged
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation
Aug 13, 2026
Merged

feat(server): in-app project hooks — live config, fail-closed, in-app approvals#1
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation

Conversation

@Defmon3

@Defmon3Defmon3 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Adds a Claude-hooks-style PreToolUse gate for Full access threads, configured per project via .t3code/hooks.json, with approvals surfaced in the app — then solidifies it:

  • Live config: .t3code/hooks.json is re-read before every hook check. Claude threads pick up create/edit/delete on the next tool call; Codex threads apply command/matcher edits on the next approval and recompute approval routing each turn.
  • Fail closed: an unreadable or invalid config turns tool calls into approval prompts (with a logged warning) instead of silently allowing.
  • No silent drops: unsupported hook event keys (PostToolUse, Stop, …) warn once in the server log.
  • Hook decisions: allow / ask / deny via stdout JSON or exit codes; Claude-compatible hookSpecificOutput accepted. Codex coverage maps command approvals → Bash, file changes → Edit.

Verification

  • vp test run over the 4 touched test files: 127/127 pass
  • vp run --filter t3 typecheck: clean in changed files
  • Two independent gpt-5.6-sol reviews over the rebased frozen scope; one low-severity warning-key collision was fixed and narrowly re-reviewed to zero surviving findings
  • Execution plan checked in at .plans/solidify-project-hooks.md; user docs updated in docs/user/permission-modes.md

🤖 Generated with Claude Code

Summary by Sourcery

Make T3 project hooks a live, fail-closed config for full-access sessions and align Codex approval routing with hook presence.

New Features:

  • Add live re-reading of .t3code/hooks.json before each PreToolUse hook evaluation so mid-session config changes take effect without restarting.
  • Expose a hasPreToolUseHooksNow signal on T3HookPlan for providers to detect current hook presence per turn.
  • Surface hook config failures as user-facing approval prompts instead of silently allowing tool calls.

Enhancements:

  • Warn once per config file about unsupported hook event keys while still running supported PreToolUse hooks.
  • Refine Codex full-access approval routing to stop requesting callbacks when a turn has no active project hooks.
  • Document project hook live-reload behavior, supported events, and unreadable-config handling in permission-modes user docs.
  • Add an internal execution plan document capturing the design and scope of the project-hooks solidification work.

Tests:

  • Extend T3HookRunner tests to cover live config creation/editing, fail-closed behavior on invalid configs, and unsupported-event warnings.
  • Adjust Codex and Claude adapter tests to account for the new hasPreToolUseHooksNow field and Codex approval routing behavior.
  • Add CodexSessionRuntime tests verifying approvalPolicy mapping when interceptApprovals is disabled in full-access mode.

@sourcery-ai

sourcery-aiBot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements live, fail-closed project hook behavior for PreToolUse across Claude and Codex runtimes, adds unsupported-event warnings, wires Codex approval routing to live hook presence, and documents the new behavior in user docs and an execution plan.

Sequence diagram for live PreToolUse evaluation and fail-closed behavior

sequenceDiagram
actor User
participant ClaudeAdapter
participant T3HookRunner
participant FileSystem
participant HookCommand
User->>ClaudeAdapter: invoke full-access tool
ClaudeAdapter->>T3HookRunner: prepare(cwd)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
T3HookRunner->>FileSystem: findConfigPath + read hooks.json
FileSystem-->>T3HookRunner: hooks.json content
T3HookRunner->>T3HookRunner: decodeHooksConfigJson
T3HookRunner->>T3HookRunner: decodeHooksConfigEventKeysJson
T3HookRunner->>T3HookRunner: logWarning on unsupported events
T3HookRunner-->>ClaudeAdapter: T3HookPlan(hasPreToolUseHooks, hasPreToolUseHooksNow)
loop each tool call
ClaudeAdapter->>T3HookRunner: plan.evaluatePreToolUse(input)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
alt hooks.json unreadable/invalid
T3HookRunner->>T3HookRunner: logConfigFailure(T3HookConfigError)
T3HookRunner-->>ClaudeAdapter: decision ask, title "T3 hook config failed"
else hooks.json valid
T3HookRunner->>HookCommand: run hook command
HookCommand-->>T3HookRunner: HookCommandOutput (allow/ask/deny)
T3HookRunner-->>ClaudeAdapter: normalizedDecision
end
end
ClaudeAdapter-->>User: tool allowed / approval prompt / denied
Loading

File-Level Changes

ChangeDetailsFiles
Make T3HookRunner re-resolve hooks config on every evaluation and expose live hook presence and fail-closed behavior.
  • Introduce HooksConfigEventKeys schema and SUPPORTED_HOOK_EVENTS to parse hook event keys from hooks.json.
  • Add warning deduping for unsupported hook events and emit Effect.logWarning with path and event details.
  • Add hasPreToolUseHooksNow effect to T3HookPlan and implement resolvePlanState helper to re-read config each time.
  • Update prepare to take an initial snapshot but have evaluatePreToolUse re-resolve config, handling T3HookConfigError by logging and returning an ask decision.
  • Add logConfigFailure helper to centralize config failure logging and reuse it in live evaluation paths.
apps/server/src/hooks/T3HookRunner.ts
Extend T3HookRunner tests to cover live config creation and edits, fail-closed behavior on invalid config, and unsupported-event warnings.
  • Add writeHooksConfig helper to create .t3code/hooks.json in a temp project directory.
  • Test that a config created after prepare toggles hasPreToolUseHooksNow to true and affects evaluation decisions.
  • Test that matcher edits are picked up without re-preparing, allowing previously matched calls when hooks no longer match.
  • Test that invalid JSON in hooks.json causes hasPreToolUseHooksNow to report hooks and evaluation to return an ask decision with a failure title and reason.
  • Test that configs declaring unsupported events (PostToolUse, Stop) log exactly one warning while still running PreToolUse hooks.
apps/server/src/hooks/T3HookRunner.test.ts
Wire CodexSessionRuntime to use spawn-time hook snapshot for initial thread open but recompute approval routing per turn from live hook presence.
  • Rename initial interceptApprovals flag to interceptApprovalsAtStart and keep using it only for openCodexThread.
  • On each sendTurn, derive interceptApprovals from options.hookPlan.hasPreToolUseHooksNow to reflect live config changes.
  • Ensure buildTurnStartParams receives the recomputed interceptApprovals for each turn.
  • Add a test that when a full-access turn has no T3 hooks, approvalPolicy is set to never while sandboxPolicy stays dangerFullAccess.
apps/server/src/provider/Layers/CodexSessionRuntime.ts
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Adjust Claude and Codex adapters and their tests to satisfy the expanded T3HookPlan interface with hasPreToolUseHooksNow.
  • Update ClaudeAdapter getHookPlan fallback plan to set hasPreToolUseHooksNow to Effect.succeed(false) when no hook runner is present.
  • Update ClaudeAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan stubs.
  • Update CodexAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan literals checked by identity.
  • Leave existing approval flows and evaluation behavior unchanged apart from the new field.
apps/server/src/provider/Layers/ClaudeAdapter.ts
apps/server/src/provider/Layers/ClaudeAdapter.test.ts
apps/server/src/provider/Layers/CodexAdapter.test.ts
Document live hook config behavior, supported events, and unreadable-config handling, and check in the execution plan for solidifying project hooks.
  • Extend permission-modes documentation with details on live hooks.json re-reads for Claude and Codex threads.
  • Describe that only PreToolUse is supported and that other events are logged once as warnings, not executed.
  • Explain fail-closed behavior when hooks.json becomes unreadable, turning tool calls into approval prompts.
  • Add .plans/solidify-project-hooks.md capturing the locked execution plan and rationale for the implementation.
docs/user/permission-modes.md
.plans/solidify-project-hooks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 13, 2026

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In readConfig, you decode the same JSON string twice (once via decodeHooksConfigJson and again via decodeHooksConfigEventKeysJson just to get event keys); consider deriving unsupportedEvents directly from config.hooks to avoid the extra parse and reduce complexity.
  • hasPreToolUseHooksNow currently uses a broad Effect.catch that logs a config failure and returns true for any error; narrowing this to T3HookConfigError (similar to the evaluatePreToolUse catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `readConfig`, you decode the same JSON string twice (once via `decodeHooksConfigJson` and again via `decodeHooksConfigEventKeysJson` just to get event keys); consider deriving `unsupportedEvents` directly from `config.hooks` to avoid the extra parse and reduce complexity.
-`hasPreToolUseHooksNow` currently uses a broad `Effect.catch` that logs a config failure and returns `true` for any error; narrowing this to `T3HookConfigError` (similar to the `evaluatePreToolUse` catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
## Individual Comments### Comment 1
<locationpath="apps/server/src/hooks/T3HookRunner.ts"line_range="384" />
<code_context>
return { decision: "allow" } satisfies T3HookDecision;
});
+ const resolvePlanState = Effect.fn("T3HookRunner.resolvePlanState")(function* (cwd: string) {
+ const configPathOption = yield* findConfigPath(cwd);
+ if (Option.isNone(configPathOption)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new hook plan flow by caching path-derived state, centralizing config-error handling, and deriving unsupported event keys from the already-decoded config instead of extra schemas.
- The extra indirection around config/state resolution and error handling is noticeable. You can keep the new live behaviors but simplify the flow and reduce repeated work.
### 1. Avoid re-running `findConfigPath` on every operation
You currently do:
```tsconst snapshot =yield*resolvePlanState(cwd);
// ...hasPreToolUseHooksNow: resolvePlanState(cwd).pipe(/* ... */),
evaluatePreToolUse: (input) =>Effect.gen(function* () {
const state =yield*resolvePlanState(cwd);
// ...
}).pipe(/* ... */),
````resolvePlanState` re-runs `findConfigPath(cwd)` every time, even though `cwd` is fixed per `prepare` call. You can keep live config re-reads but cache the `configPath` and `projectDirectory` once and re-use them:
```tsconst prepare:T3HookRunner["Service"]["prepare"] =Effect.fn("T3HookRunner.prepare")(function* (cwd) {
const configPathOption =yield*findConfigPath(cwd);
if (Option.isNone(configPathOption)) {
return {
configPath: undefined,
hasPreToolUseHooks: false,
hasPreToolUseHooksNow: Effect.succeed(false),
evaluatePreToolUse: () =>Effect.succeed({ decision: "allow"asconst }),
} satisfiesT3HookPlan;
}
const configPath =configPathOption.value;
const projectDirectory =path.dirname(path.dirname(configPath));
const readCurrentState =Effect.gen(function* () {
const config =yield*readConfig(configPath);
return {
configPath,
projectDirectory,
entries: config.hooks.PreToolUse?? ([] asReadonlyArray<HookMatcherConfig>),
};
});
const initialState =yield*readCurrentState;
const hasHooks =initialState.entries.length>0;
return {
configPath,
hasPreToolUseHooks: hasHooks,
hasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>logConfigFailure(error).pipe(Effect.as(true))),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
Effect.flatMap((state) =>state.entries.length===0?Effect.succeed({ decision: "allow" } satisfiesT3HookDecision)
:evaluateEntries({
entries: state.entries,
configPath: state.configPath,
projectDirectory: state.projectDirectory!,
payload: { ...input, cwd },
}),
),
Effect.catchTag(
"T3HookConfigError",
handleConfigFailure, // see helper below
),
),
} satisfiesT3HookPlan;
});
```
This keeps:
- Single `findConfigPath` per `prepare`.
- Live config re-reading via `readCurrentState`.
- Snapshot fields (`configPath`, `hasPreToolUseHooks`) plus dynamic ones, but with clearer boundaries.
### 2. Centralize error → decision mapping
Right now the mapping from `T3HookConfigError` to `T3HookDecision` is inlined inside `prepare`:
```tsEffect.catchTag("T3HookConfigError", (error) =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
),
),
```
Encapsulating this makes both `hasPreToolUseHooksNow` and `evaluatePreToolUse` easier to read:
```tsconst handleConfigFailure = (error:T3HookConfigError):Effect.Effect<T3HookDecision> =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
);
```
Then:
```tshasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>errorinstanceofT3HookConfigError?handleConfigFailure(error).pipe(Effect.map(() =>true))
:logConfigFailure(errorasT3HookConfigError).pipe(Effect.as(true)),
),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
// ...Effect.catchTag("T3HookConfigError", handleConfigFailure),
),
```
This keeps your UX behavior identical but removes duplicated decision construction and shortens the pipelines.
### 3. Simplify unsupported-event warning
You added a second schema + decoder just to get event keys:
```tsconst HooksConfigEventKeys =Schema.Struct({
hooks: Schema.Record(Schema.String, Schema.Unknown),
});
const decodeHooksConfigEventKeysJson =Schema.decodeUnknownEffect(
fromLenientJson(HooksConfigEventKeys),
);
const declaredEvents =yield*decodeHooksConfigEventKeysJson(raw).pipe(
Effect.map((decoded) =>Object.keys(decoded.hooks)),
Effect.orElseSucceed(() => [] asReadonlyArray<string>),
);
```
Since you already successfully decoded `config` above, you can derive event keys directly from it and drop the extra schema/decoder:
```tsconst config =yield*decodeHooksConfigJson(raw);
// ...const declaredEvents =Object.keys(config.hooks);
const unsupportedEvents =declaredEvents
.filter((event) =>!SUPPORTED_HOOK_EVENTS.includes(eventas (typeofSUPPORTED_HOOK_EVENTS)[number]))
.sort();
if (unsupportedEvents.length>0) {
if (!warnedUnsupportedEvents.has(configPath)) {
warnedUnsupportedEvents.add(configPath);
yield*Effect.logWarning("ignoring unsupported T3 hook events", {
path: configPath,
unsupportedEvents,
supportedEvents: SUPPORTED_HOOK_EVENTS,
});
}
}
```
If “warn once per config file” is sufficient, keying `warnedUnsupportedEvents` by `configPath` alone removes the need to build and track `warningKey` strings.
These changes keep all current behaviors (live config updates, fail-closed, warnings) but reduce indirection, duplicated logic, and statefulness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadapps/server/src/hooks/T3HookRunner.ts
Defmon3and others added 3 commits August 13, 2026 14:56
Re-resolve .t3code/hooks.json on every PreToolUse evaluation so mid-session
edits, creation, and deletion take effect without a restart; recompute Codex
approval routing per turn from the live config; fail closed (ask) with a
logged warning when the config becomes unreadable; warn once per config on
unsupported hook event keys instead of silently ignoring them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Defmon3
Defmon3force-pushed the feat/in-app-hook-confirmation branch from 6e304cb to 83d9fd8CompareAugust 13, 2026 13:05
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 83d9fd8.

This comment will update automatically after the next completed run.

@Defmon3
Defmon3 merged commit 75288e8 into customAug 13, 2026
8 of 12 checks passed
@Defmon3
Defmon3 deleted the feat/in-app-hook-confirmation branch August 13, 2026 13:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Defmon3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(server): in-app project hooks — live config, fail-closed, in-app approvals - #1

Merged
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation
Aug 13, 2026
Merged

feat(server): in-app project hooks — live config, fail-closed, in-app approvals#1
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation

Conversation

@Defmon3

@Defmon3Defmon3 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Adds a Claude-hooks-style PreToolUse gate for Full access threads, configured per project via .t3code/hooks.json, with approvals surfaced in the app — then solidifies it:

  • Live config: .t3code/hooks.json is re-read before every hook check. Claude threads pick up create/edit/delete on the next tool call; Codex threads apply command/matcher edits on the next approval and recompute approval routing each turn.
  • Fail closed: an unreadable or invalid config turns tool calls into approval prompts (with a logged warning) instead of silently allowing.
  • No silent drops: unsupported hook event keys (PostToolUse, Stop, …) warn once in the server log.
  • Hook decisions: allow / ask / deny via stdout JSON or exit codes; Claude-compatible hookSpecificOutput accepted. Codex coverage maps command approvals → Bash, file changes → Edit.

Verification

  • vp test run over the 4 touched test files: 127/127 pass
  • vp run --filter t3 typecheck: clean in changed files
  • Two independent gpt-5.6-sol reviews over the rebased frozen scope; one low-severity warning-key collision was fixed and narrowly re-reviewed to zero surviving findings
  • Execution plan checked in at .plans/solidify-project-hooks.md; user docs updated in docs/user/permission-modes.md

🤖 Generated with Claude Code

Summary by Sourcery

Make T3 project hooks a live, fail-closed config for full-access sessions and align Codex approval routing with hook presence.

New Features:

  • Add live re-reading of .t3code/hooks.json before each PreToolUse hook evaluation so mid-session config changes take effect without restarting.
  • Expose a hasPreToolUseHooksNow signal on T3HookPlan for providers to detect current hook presence per turn.
  • Surface hook config failures as user-facing approval prompts instead of silently allowing tool calls.

Enhancements:

  • Warn once per config file about unsupported hook event keys while still running supported PreToolUse hooks.
  • Refine Codex full-access approval routing to stop requesting callbacks when a turn has no active project hooks.
  • Document project hook live-reload behavior, supported events, and unreadable-config handling in permission-modes user docs.
  • Add an internal execution plan document capturing the design and scope of the project-hooks solidification work.

Tests:

  • Extend T3HookRunner tests to cover live config creation/editing, fail-closed behavior on invalid configs, and unsupported-event warnings.
  • Adjust Codex and Claude adapter tests to account for the new hasPreToolUseHooksNow field and Codex approval routing behavior.
  • Add CodexSessionRuntime tests verifying approvalPolicy mapping when interceptApprovals is disabled in full-access mode.

@sourcery-ai

sourcery-aiBot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements live, fail-closed project hook behavior for PreToolUse across Claude and Codex runtimes, adds unsupported-event warnings, wires Codex approval routing to live hook presence, and documents the new behavior in user docs and an execution plan.

Sequence diagram for live PreToolUse evaluation and fail-closed behavior

sequenceDiagram
actor User
participant ClaudeAdapter
participant T3HookRunner
participant FileSystem
participant HookCommand
User->>ClaudeAdapter: invoke full-access tool
ClaudeAdapter->>T3HookRunner: prepare(cwd)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
T3HookRunner->>FileSystem: findConfigPath + read hooks.json
FileSystem-->>T3HookRunner: hooks.json content
T3HookRunner->>T3HookRunner: decodeHooksConfigJson
T3HookRunner->>T3HookRunner: decodeHooksConfigEventKeysJson
T3HookRunner->>T3HookRunner: logWarning on unsupported events
T3HookRunner-->>ClaudeAdapter: T3HookPlan(hasPreToolUseHooks, hasPreToolUseHooksNow)
loop each tool call
ClaudeAdapter->>T3HookRunner: plan.evaluatePreToolUse(input)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
alt hooks.json unreadable/invalid
T3HookRunner->>T3HookRunner: logConfigFailure(T3HookConfigError)
T3HookRunner-->>ClaudeAdapter: decision ask, title "T3 hook config failed"
else hooks.json valid
T3HookRunner->>HookCommand: run hook command
HookCommand-->>T3HookRunner: HookCommandOutput (allow/ask/deny)
T3HookRunner-->>ClaudeAdapter: normalizedDecision
end
end
ClaudeAdapter-->>User: tool allowed / approval prompt / denied
Loading

File-Level Changes

ChangeDetailsFiles
Make T3HookRunner re-resolve hooks config on every evaluation and expose live hook presence and fail-closed behavior.
  • Introduce HooksConfigEventKeys schema and SUPPORTED_HOOK_EVENTS to parse hook event keys from hooks.json.
  • Add warning deduping for unsupported hook events and emit Effect.logWarning with path and event details.
  • Add hasPreToolUseHooksNow effect to T3HookPlan and implement resolvePlanState helper to re-read config each time.
  • Update prepare to take an initial snapshot but have evaluatePreToolUse re-resolve config, handling T3HookConfigError by logging and returning an ask decision.
  • Add logConfigFailure helper to centralize config failure logging and reuse it in live evaluation paths.
apps/server/src/hooks/T3HookRunner.ts
Extend T3HookRunner tests to cover live config creation and edits, fail-closed behavior on invalid config, and unsupported-event warnings.
  • Add writeHooksConfig helper to create .t3code/hooks.json in a temp project directory.
  • Test that a config created after prepare toggles hasPreToolUseHooksNow to true and affects evaluation decisions.
  • Test that matcher edits are picked up without re-preparing, allowing previously matched calls when hooks no longer match.
  • Test that invalid JSON in hooks.json causes hasPreToolUseHooksNow to report hooks and evaluation to return an ask decision with a failure title and reason.
  • Test that configs declaring unsupported events (PostToolUse, Stop) log exactly one warning while still running PreToolUse hooks.
apps/server/src/hooks/T3HookRunner.test.ts
Wire CodexSessionRuntime to use spawn-time hook snapshot for initial thread open but recompute approval routing per turn from live hook presence.
  • Rename initial interceptApprovals flag to interceptApprovalsAtStart and keep using it only for openCodexThread.
  • On each sendTurn, derive interceptApprovals from options.hookPlan.hasPreToolUseHooksNow to reflect live config changes.
  • Ensure buildTurnStartParams receives the recomputed interceptApprovals for each turn.
  • Add a test that when a full-access turn has no T3 hooks, approvalPolicy is set to never while sandboxPolicy stays dangerFullAccess.
apps/server/src/provider/Layers/CodexSessionRuntime.ts
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Adjust Claude and Codex adapters and their tests to satisfy the expanded T3HookPlan interface with hasPreToolUseHooksNow.
  • Update ClaudeAdapter getHookPlan fallback plan to set hasPreToolUseHooksNow to Effect.succeed(false) when no hook runner is present.
  • Update ClaudeAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan stubs.
  • Update CodexAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan literals checked by identity.
  • Leave existing approval flows and evaluation behavior unchanged apart from the new field.
apps/server/src/provider/Layers/ClaudeAdapter.ts
apps/server/src/provider/Layers/ClaudeAdapter.test.ts
apps/server/src/provider/Layers/CodexAdapter.test.ts
Document live hook config behavior, supported events, and unreadable-config handling, and check in the execution plan for solidifying project hooks.
  • Extend permission-modes documentation with details on live hooks.json re-reads for Claude and Codex threads.
  • Describe that only PreToolUse is supported and that other events are logged once as warnings, not executed.
  • Explain fail-closed behavior when hooks.json becomes unreadable, turning tool calls into approval prompts.
  • Add .plans/solidify-project-hooks.md capturing the locked execution plan and rationale for the implementation.
docs/user/permission-modes.md
.plans/solidify-project-hooks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 13, 2026

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In readConfig, you decode the same JSON string twice (once via decodeHooksConfigJson and again via decodeHooksConfigEventKeysJson just to get event keys); consider deriving unsupportedEvents directly from config.hooks to avoid the extra parse and reduce complexity.
  • hasPreToolUseHooksNow currently uses a broad Effect.catch that logs a config failure and returns true for any error; narrowing this to T3HookConfigError (similar to the evaluatePreToolUse catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `readConfig`, you decode the same JSON string twice (once via `decodeHooksConfigJson` and again via `decodeHooksConfigEventKeysJson` just to get event keys); consider deriving `unsupportedEvents` directly from `config.hooks` to avoid the extra parse and reduce complexity.
-`hasPreToolUseHooksNow` currently uses a broad `Effect.catch` that logs a config failure and returns `true` for any error; narrowing this to `T3HookConfigError` (similar to the `evaluatePreToolUse` catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
## Individual Comments### Comment 1
<locationpath="apps/server/src/hooks/T3HookRunner.ts"line_range="384" />
<code_context>
return { decision: "allow" } satisfies T3HookDecision;
});
+ const resolvePlanState = Effect.fn("T3HookRunner.resolvePlanState")(function* (cwd: string) {
+ const configPathOption = yield* findConfigPath(cwd);
+ if (Option.isNone(configPathOption)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new hook plan flow by caching path-derived state, centralizing config-error handling, and deriving unsupported event keys from the already-decoded config instead of extra schemas.
- The extra indirection around config/state resolution and error handling is noticeable. You can keep the new live behaviors but simplify the flow and reduce repeated work.
### 1. Avoid re-running `findConfigPath` on every operation
You currently do:
```tsconst snapshot =yield*resolvePlanState(cwd);
// ...hasPreToolUseHooksNow: resolvePlanState(cwd).pipe(/* ... */),
evaluatePreToolUse: (input) =>Effect.gen(function* () {
const state =yield*resolvePlanState(cwd);
// ...
}).pipe(/* ... */),
````resolvePlanState` re-runs `findConfigPath(cwd)` every time, even though `cwd` is fixed per `prepare` call. You can keep live config re-reads but cache the `configPath` and `projectDirectory` once and re-use them:
```tsconst prepare:T3HookRunner["Service"]["prepare"] =Effect.fn("T3HookRunner.prepare")(function* (cwd) {
const configPathOption =yield*findConfigPath(cwd);
if (Option.isNone(configPathOption)) {
return {
configPath: undefined,
hasPreToolUseHooks: false,
hasPreToolUseHooksNow: Effect.succeed(false),
evaluatePreToolUse: () =>Effect.succeed({ decision: "allow"asconst }),
} satisfiesT3HookPlan;
}
const configPath =configPathOption.value;
const projectDirectory =path.dirname(path.dirname(configPath));
const readCurrentState =Effect.gen(function* () {
const config =yield*readConfig(configPath);
return {
configPath,
projectDirectory,
entries: config.hooks.PreToolUse?? ([] asReadonlyArray<HookMatcherConfig>),
};
});
const initialState =yield*readCurrentState;
const hasHooks =initialState.entries.length>0;
return {
configPath,
hasPreToolUseHooks: hasHooks,
hasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>logConfigFailure(error).pipe(Effect.as(true))),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
Effect.flatMap((state) =>state.entries.length===0?Effect.succeed({ decision: "allow" } satisfiesT3HookDecision)
:evaluateEntries({
entries: state.entries,
configPath: state.configPath,
projectDirectory: state.projectDirectory!,
payload: { ...input, cwd },
}),
),
Effect.catchTag(
"T3HookConfigError",
handleConfigFailure, // see helper below
),
),
} satisfiesT3HookPlan;
});
```
This keeps:
- Single `findConfigPath` per `prepare`.
- Live config re-reading via `readCurrentState`.
- Snapshot fields (`configPath`, `hasPreToolUseHooks`) plus dynamic ones, but with clearer boundaries.
### 2. Centralize error → decision mapping
Right now the mapping from `T3HookConfigError` to `T3HookDecision` is inlined inside `prepare`:
```tsEffect.catchTag("T3HookConfigError", (error) =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
),
),
```
Encapsulating this makes both `hasPreToolUseHooksNow` and `evaluatePreToolUse` easier to read:
```tsconst handleConfigFailure = (error:T3HookConfigError):Effect.Effect<T3HookDecision> =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
);
```
Then:
```tshasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>errorinstanceofT3HookConfigError?handleConfigFailure(error).pipe(Effect.map(() =>true))
:logConfigFailure(errorasT3HookConfigError).pipe(Effect.as(true)),
),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
// ...Effect.catchTag("T3HookConfigError", handleConfigFailure),
),
```
This keeps your UX behavior identical but removes duplicated decision construction and shortens the pipelines.
### 3. Simplify unsupported-event warning
You added a second schema + decoder just to get event keys:
```tsconst HooksConfigEventKeys =Schema.Struct({
hooks: Schema.Record(Schema.String, Schema.Unknown),
});
const decodeHooksConfigEventKeysJson =Schema.decodeUnknownEffect(
fromLenientJson(HooksConfigEventKeys),
);
const declaredEvents =yield*decodeHooksConfigEventKeysJson(raw).pipe(
Effect.map((decoded) =>Object.keys(decoded.hooks)),
Effect.orElseSucceed(() => [] asReadonlyArray<string>),
);
```
Since you already successfully decoded `config` above, you can derive event keys directly from it and drop the extra schema/decoder:
```tsconst config =yield*decodeHooksConfigJson(raw);
// ...const declaredEvents =Object.keys(config.hooks);
const unsupportedEvents =declaredEvents
.filter((event) =>!SUPPORTED_HOOK_EVENTS.includes(eventas (typeofSUPPORTED_HOOK_EVENTS)[number]))
.sort();
if (unsupportedEvents.length>0) {
if (!warnedUnsupportedEvents.has(configPath)) {
warnedUnsupportedEvents.add(configPath);
yield*Effect.logWarning("ignoring unsupported T3 hook events", {
path: configPath,
unsupportedEvents,
supportedEvents: SUPPORTED_HOOK_EVENTS,
});
}
}
```
If “warn once per config file” is sufficient, keying `warnedUnsupportedEvents` by `configPath` alone removes the need to build and track `warningKey` strings.
These changes keep all current behaviors (live config updates, fail-closed, warnings) but reduce indirection, duplicated logic, and statefulness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadapps/server/src/hooks/T3HookRunner.ts
Defmon3and others added 3 commits August 13, 2026 14:56
Re-resolve .t3code/hooks.json on every PreToolUse evaluation so mid-session
edits, creation, and deletion take effect without a restart; recompute Codex
approval routing per turn from the live config; fail closed (ask) with a
logged warning when the config becomes unreadable; warn once per config on
unsupported hook event keys instead of silently ignoring them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Defmon3
Defmon3force-pushed the feat/in-app-hook-confirmation branch from 6e304cb to 83d9fd8CompareAugust 13, 2026 13:05
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 83d9fd8.

This comment will update automatically after the next completed run.

@Defmon3
Defmon3 merged commit 75288e8 into customAug 13, 2026
8 of 12 checks passed
@Defmon3
Defmon3 deleted the feat/in-app-hook-confirmation branch August 13, 2026 13:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Defmon3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(server): in-app project hooks — live config, fail-closed, in-app approvals - #1

Merged
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation
Aug 13, 2026
Merged

feat(server): in-app project hooks — live config, fail-closed, in-app approvals#1
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation

Conversation

@Defmon3

@Defmon3Defmon3 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Adds a Claude-hooks-style PreToolUse gate for Full access threads, configured per project via .t3code/hooks.json, with approvals surfaced in the app — then solidifies it:

  • Live config: .t3code/hooks.json is re-read before every hook check. Claude threads pick up create/edit/delete on the next tool call; Codex threads apply command/matcher edits on the next approval and recompute approval routing each turn.
  • Fail closed: an unreadable or invalid config turns tool calls into approval prompts (with a logged warning) instead of silently allowing.
  • No silent drops: unsupported hook event keys (PostToolUse, Stop, …) warn once in the server log.
  • Hook decisions: allow / ask / deny via stdout JSON or exit codes; Claude-compatible hookSpecificOutput accepted. Codex coverage maps command approvals → Bash, file changes → Edit.

Verification

  • vp test run over the 4 touched test files: 127/127 pass
  • vp run --filter t3 typecheck: clean in changed files
  • Two independent gpt-5.6-sol reviews over the rebased frozen scope; one low-severity warning-key collision was fixed and narrowly re-reviewed to zero surviving findings
  • Execution plan checked in at .plans/solidify-project-hooks.md; user docs updated in docs/user/permission-modes.md

🤖 Generated with Claude Code

Summary by Sourcery

Make T3 project hooks a live, fail-closed config for full-access sessions and align Codex approval routing with hook presence.

New Features:

  • Add live re-reading of .t3code/hooks.json before each PreToolUse hook evaluation so mid-session config changes take effect without restarting.
  • Expose a hasPreToolUseHooksNow signal on T3HookPlan for providers to detect current hook presence per turn.
  • Surface hook config failures as user-facing approval prompts instead of silently allowing tool calls.

Enhancements:

  • Warn once per config file about unsupported hook event keys while still running supported PreToolUse hooks.
  • Refine Codex full-access approval routing to stop requesting callbacks when a turn has no active project hooks.
  • Document project hook live-reload behavior, supported events, and unreadable-config handling in permission-modes user docs.
  • Add an internal execution plan document capturing the design and scope of the project-hooks solidification work.

Tests:

  • Extend T3HookRunner tests to cover live config creation/editing, fail-closed behavior on invalid configs, and unsupported-event warnings.
  • Adjust Codex and Claude adapter tests to account for the new hasPreToolUseHooksNow field and Codex approval routing behavior.
  • Add CodexSessionRuntime tests verifying approvalPolicy mapping when interceptApprovals is disabled in full-access mode.

@sourcery-ai

sourcery-aiBot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements live, fail-closed project hook behavior for PreToolUse across Claude and Codex runtimes, adds unsupported-event warnings, wires Codex approval routing to live hook presence, and documents the new behavior in user docs and an execution plan.

Sequence diagram for live PreToolUse evaluation and fail-closed behavior

sequenceDiagram
actor User
participant ClaudeAdapter
participant T3HookRunner
participant FileSystem
participant HookCommand
User->>ClaudeAdapter: invoke full-access tool
ClaudeAdapter->>T3HookRunner: prepare(cwd)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
T3HookRunner->>FileSystem: findConfigPath + read hooks.json
FileSystem-->>T3HookRunner: hooks.json content
T3HookRunner->>T3HookRunner: decodeHooksConfigJson
T3HookRunner->>T3HookRunner: decodeHooksConfigEventKeysJson
T3HookRunner->>T3HookRunner: logWarning on unsupported events
T3HookRunner-->>ClaudeAdapter: T3HookPlan(hasPreToolUseHooks, hasPreToolUseHooksNow)
loop each tool call
ClaudeAdapter->>T3HookRunner: plan.evaluatePreToolUse(input)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
alt hooks.json unreadable/invalid
T3HookRunner->>T3HookRunner: logConfigFailure(T3HookConfigError)
T3HookRunner-->>ClaudeAdapter: decision ask, title "T3 hook config failed"
else hooks.json valid
T3HookRunner->>HookCommand: run hook command
HookCommand-->>T3HookRunner: HookCommandOutput (allow/ask/deny)
T3HookRunner-->>ClaudeAdapter: normalizedDecision
end
end
ClaudeAdapter-->>User: tool allowed / approval prompt / denied
Loading

File-Level Changes

ChangeDetailsFiles
Make T3HookRunner re-resolve hooks config on every evaluation and expose live hook presence and fail-closed behavior.
  • Introduce HooksConfigEventKeys schema and SUPPORTED_HOOK_EVENTS to parse hook event keys from hooks.json.
  • Add warning deduping for unsupported hook events and emit Effect.logWarning with path and event details.
  • Add hasPreToolUseHooksNow effect to T3HookPlan and implement resolvePlanState helper to re-read config each time.
  • Update prepare to take an initial snapshot but have evaluatePreToolUse re-resolve config, handling T3HookConfigError by logging and returning an ask decision.
  • Add logConfigFailure helper to centralize config failure logging and reuse it in live evaluation paths.
apps/server/src/hooks/T3HookRunner.ts
Extend T3HookRunner tests to cover live config creation and edits, fail-closed behavior on invalid config, and unsupported-event warnings.
  • Add writeHooksConfig helper to create .t3code/hooks.json in a temp project directory.
  • Test that a config created after prepare toggles hasPreToolUseHooksNow to true and affects evaluation decisions.
  • Test that matcher edits are picked up without re-preparing, allowing previously matched calls when hooks no longer match.
  • Test that invalid JSON in hooks.json causes hasPreToolUseHooksNow to report hooks and evaluation to return an ask decision with a failure title and reason.
  • Test that configs declaring unsupported events (PostToolUse, Stop) log exactly one warning while still running PreToolUse hooks.
apps/server/src/hooks/T3HookRunner.test.ts
Wire CodexSessionRuntime to use spawn-time hook snapshot for initial thread open but recompute approval routing per turn from live hook presence.
  • Rename initial interceptApprovals flag to interceptApprovalsAtStart and keep using it only for openCodexThread.
  • On each sendTurn, derive interceptApprovals from options.hookPlan.hasPreToolUseHooksNow to reflect live config changes.
  • Ensure buildTurnStartParams receives the recomputed interceptApprovals for each turn.
  • Add a test that when a full-access turn has no T3 hooks, approvalPolicy is set to never while sandboxPolicy stays dangerFullAccess.
apps/server/src/provider/Layers/CodexSessionRuntime.ts
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Adjust Claude and Codex adapters and their tests to satisfy the expanded T3HookPlan interface with hasPreToolUseHooksNow.
  • Update ClaudeAdapter getHookPlan fallback plan to set hasPreToolUseHooksNow to Effect.succeed(false) when no hook runner is present.
  • Update ClaudeAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan stubs.
  • Update CodexAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan literals checked by identity.
  • Leave existing approval flows and evaluation behavior unchanged apart from the new field.
apps/server/src/provider/Layers/ClaudeAdapter.ts
apps/server/src/provider/Layers/ClaudeAdapter.test.ts
apps/server/src/provider/Layers/CodexAdapter.test.ts
Document live hook config behavior, supported events, and unreadable-config handling, and check in the execution plan for solidifying project hooks.
  • Extend permission-modes documentation with details on live hooks.json re-reads for Claude and Codex threads.
  • Describe that only PreToolUse is supported and that other events are logged once as warnings, not executed.
  • Explain fail-closed behavior when hooks.json becomes unreadable, turning tool calls into approval prompts.
  • Add .plans/solidify-project-hooks.md capturing the locked execution plan and rationale for the implementation.
docs/user/permission-modes.md
.plans/solidify-project-hooks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 13, 2026

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In readConfig, you decode the same JSON string twice (once via decodeHooksConfigJson and again via decodeHooksConfigEventKeysJson just to get event keys); consider deriving unsupportedEvents directly from config.hooks to avoid the extra parse and reduce complexity.
  • hasPreToolUseHooksNow currently uses a broad Effect.catch that logs a config failure and returns true for any error; narrowing this to T3HookConfigError (similar to the evaluatePreToolUse catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `readConfig`, you decode the same JSON string twice (once via `decodeHooksConfigJson` and again via `decodeHooksConfigEventKeysJson` just to get event keys); consider deriving `unsupportedEvents` directly from `config.hooks` to avoid the extra parse and reduce complexity.
-`hasPreToolUseHooksNow` currently uses a broad `Effect.catch` that logs a config failure and returns `true` for any error; narrowing this to `T3HookConfigError` (similar to the `evaluatePreToolUse` catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
## Individual Comments### Comment 1
<locationpath="apps/server/src/hooks/T3HookRunner.ts"line_range="384" />
<code_context>
return { decision: "allow" } satisfies T3HookDecision;
});
+ const resolvePlanState = Effect.fn("T3HookRunner.resolvePlanState")(function* (cwd: string) {
+ const configPathOption = yield* findConfigPath(cwd);
+ if (Option.isNone(configPathOption)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new hook plan flow by caching path-derived state, centralizing config-error handling, and deriving unsupported event keys from the already-decoded config instead of extra schemas.
- The extra indirection around config/state resolution and error handling is noticeable. You can keep the new live behaviors but simplify the flow and reduce repeated work.
### 1. Avoid re-running `findConfigPath` on every operation
You currently do:
```tsconst snapshot =yield*resolvePlanState(cwd);
// ...hasPreToolUseHooksNow: resolvePlanState(cwd).pipe(/* ... */),
evaluatePreToolUse: (input) =>Effect.gen(function* () {
const state =yield*resolvePlanState(cwd);
// ...
}).pipe(/* ... */),
````resolvePlanState` re-runs `findConfigPath(cwd)` every time, even though `cwd` is fixed per `prepare` call. You can keep live config re-reads but cache the `configPath` and `projectDirectory` once and re-use them:
```tsconst prepare:T3HookRunner["Service"]["prepare"] =Effect.fn("T3HookRunner.prepare")(function* (cwd) {
const configPathOption =yield*findConfigPath(cwd);
if (Option.isNone(configPathOption)) {
return {
configPath: undefined,
hasPreToolUseHooks: false,
hasPreToolUseHooksNow: Effect.succeed(false),
evaluatePreToolUse: () =>Effect.succeed({ decision: "allow"asconst }),
} satisfiesT3HookPlan;
}
const configPath =configPathOption.value;
const projectDirectory =path.dirname(path.dirname(configPath));
const readCurrentState =Effect.gen(function* () {
const config =yield*readConfig(configPath);
return {
configPath,
projectDirectory,
entries: config.hooks.PreToolUse?? ([] asReadonlyArray<HookMatcherConfig>),
};
});
const initialState =yield*readCurrentState;
const hasHooks =initialState.entries.length>0;
return {
configPath,
hasPreToolUseHooks: hasHooks,
hasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>logConfigFailure(error).pipe(Effect.as(true))),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
Effect.flatMap((state) =>state.entries.length===0?Effect.succeed({ decision: "allow" } satisfiesT3HookDecision)
:evaluateEntries({
entries: state.entries,
configPath: state.configPath,
projectDirectory: state.projectDirectory!,
payload: { ...input, cwd },
}),
),
Effect.catchTag(
"T3HookConfigError",
handleConfigFailure, // see helper below
),
),
} satisfiesT3HookPlan;
});
```
This keeps:
- Single `findConfigPath` per `prepare`.
- Live config re-reading via `readCurrentState`.
- Snapshot fields (`configPath`, `hasPreToolUseHooks`) plus dynamic ones, but with clearer boundaries.
### 2. Centralize error → decision mapping
Right now the mapping from `T3HookConfigError` to `T3HookDecision` is inlined inside `prepare`:
```tsEffect.catchTag("T3HookConfigError", (error) =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
),
),
```
Encapsulating this makes both `hasPreToolUseHooksNow` and `evaluatePreToolUse` easier to read:
```tsconst handleConfigFailure = (error:T3HookConfigError):Effect.Effect<T3HookDecision> =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
);
```
Then:
```tshasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>errorinstanceofT3HookConfigError?handleConfigFailure(error).pipe(Effect.map(() =>true))
:logConfigFailure(errorasT3HookConfigError).pipe(Effect.as(true)),
),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
// ...Effect.catchTag("T3HookConfigError", handleConfigFailure),
),
```
This keeps your UX behavior identical but removes duplicated decision construction and shortens the pipelines.
### 3. Simplify unsupported-event warning
You added a second schema + decoder just to get event keys:
```tsconst HooksConfigEventKeys =Schema.Struct({
hooks: Schema.Record(Schema.String, Schema.Unknown),
});
const decodeHooksConfigEventKeysJson =Schema.decodeUnknownEffect(
fromLenientJson(HooksConfigEventKeys),
);
const declaredEvents =yield*decodeHooksConfigEventKeysJson(raw).pipe(
Effect.map((decoded) =>Object.keys(decoded.hooks)),
Effect.orElseSucceed(() => [] asReadonlyArray<string>),
);
```
Since you already successfully decoded `config` above, you can derive event keys directly from it and drop the extra schema/decoder:
```tsconst config =yield*decodeHooksConfigJson(raw);
// ...const declaredEvents =Object.keys(config.hooks);
const unsupportedEvents =declaredEvents
.filter((event) =>!SUPPORTED_HOOK_EVENTS.includes(eventas (typeofSUPPORTED_HOOK_EVENTS)[number]))
.sort();
if (unsupportedEvents.length>0) {
if (!warnedUnsupportedEvents.has(configPath)) {
warnedUnsupportedEvents.add(configPath);
yield*Effect.logWarning("ignoring unsupported T3 hook events", {
path: configPath,
unsupportedEvents,
supportedEvents: SUPPORTED_HOOK_EVENTS,
});
}
}
```
If “warn once per config file” is sufficient, keying `warnedUnsupportedEvents` by `configPath` alone removes the need to build and track `warningKey` strings.
These changes keep all current behaviors (live config updates, fail-closed, warnings) but reduce indirection, duplicated logic, and statefulness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadapps/server/src/hooks/T3HookRunner.ts
Defmon3and others added 3 commits August 13, 2026 14:56
Re-resolve .t3code/hooks.json on every PreToolUse evaluation so mid-session
edits, creation, and deletion take effect without a restart; recompute Codex
approval routing per turn from the live config; fail closed (ask) with a
logged warning when the config becomes unreadable; warn once per config on
unsupported hook event keys instead of silently ignoring them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Defmon3
Defmon3force-pushed the feat/in-app-hook-confirmation branch from 6e304cb to 83d9fd8CompareAugust 13, 2026 13:05
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 83d9fd8.

This comment will update automatically after the next completed run.

@Defmon3
Defmon3 merged commit 75288e8 into customAug 13, 2026
8 of 12 checks passed
@Defmon3
Defmon3 deleted the feat/in-app-hook-confirmation branch August 13, 2026 13:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Defmon3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(server): in-app project hooks — live config, fail-closed, in-app approvals - #1

Merged
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation
Aug 13, 2026
Merged

feat(server): in-app project hooks — live config, fail-closed, in-app approvals#1
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation

Conversation

@Defmon3

@Defmon3Defmon3 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Adds a Claude-hooks-style PreToolUse gate for Full access threads, configured per project via .t3code/hooks.json, with approvals surfaced in the app — then solidifies it:

  • Live config: .t3code/hooks.json is re-read before every hook check. Claude threads pick up create/edit/delete on the next tool call; Codex threads apply command/matcher edits on the next approval and recompute approval routing each turn.
  • Fail closed: an unreadable or invalid config turns tool calls into approval prompts (with a logged warning) instead of silently allowing.
  • No silent drops: unsupported hook event keys (PostToolUse, Stop, …) warn once in the server log.
  • Hook decisions: allow / ask / deny via stdout JSON or exit codes; Claude-compatible hookSpecificOutput accepted. Codex coverage maps command approvals → Bash, file changes → Edit.

Verification

  • vp test run over the 4 touched test files: 127/127 pass
  • vp run --filter t3 typecheck: clean in changed files
  • Two independent gpt-5.6-sol reviews over the rebased frozen scope; one low-severity warning-key collision was fixed and narrowly re-reviewed to zero surviving findings
  • Execution plan checked in at .plans/solidify-project-hooks.md; user docs updated in docs/user/permission-modes.md

🤖 Generated with Claude Code

Summary by Sourcery

Make T3 project hooks a live, fail-closed config for full-access sessions and align Codex approval routing with hook presence.

New Features:

  • Add live re-reading of .t3code/hooks.json before each PreToolUse hook evaluation so mid-session config changes take effect without restarting.
  • Expose a hasPreToolUseHooksNow signal on T3HookPlan for providers to detect current hook presence per turn.
  • Surface hook config failures as user-facing approval prompts instead of silently allowing tool calls.

Enhancements:

  • Warn once per config file about unsupported hook event keys while still running supported PreToolUse hooks.
  • Refine Codex full-access approval routing to stop requesting callbacks when a turn has no active project hooks.
  • Document project hook live-reload behavior, supported events, and unreadable-config handling in permission-modes user docs.
  • Add an internal execution plan document capturing the design and scope of the project-hooks solidification work.

Tests:

  • Extend T3HookRunner tests to cover live config creation/editing, fail-closed behavior on invalid configs, and unsupported-event warnings.
  • Adjust Codex and Claude adapter tests to account for the new hasPreToolUseHooksNow field and Codex approval routing behavior.
  • Add CodexSessionRuntime tests verifying approvalPolicy mapping when interceptApprovals is disabled in full-access mode.

@sourcery-ai

sourcery-aiBot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements live, fail-closed project hook behavior for PreToolUse across Claude and Codex runtimes, adds unsupported-event warnings, wires Codex approval routing to live hook presence, and documents the new behavior in user docs and an execution plan.

Sequence diagram for live PreToolUse evaluation and fail-closed behavior

sequenceDiagram
actor User
participant ClaudeAdapter
participant T3HookRunner
participant FileSystem
participant HookCommand
User->>ClaudeAdapter: invoke full-access tool
ClaudeAdapter->>T3HookRunner: prepare(cwd)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
T3HookRunner->>FileSystem: findConfigPath + read hooks.json
FileSystem-->>T3HookRunner: hooks.json content
T3HookRunner->>T3HookRunner: decodeHooksConfigJson
T3HookRunner->>T3HookRunner: decodeHooksConfigEventKeysJson
T3HookRunner->>T3HookRunner: logWarning on unsupported events
T3HookRunner-->>ClaudeAdapter: T3HookPlan(hasPreToolUseHooks, hasPreToolUseHooksNow)
loop each tool call
ClaudeAdapter->>T3HookRunner: plan.evaluatePreToolUse(input)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
alt hooks.json unreadable/invalid
T3HookRunner->>T3HookRunner: logConfigFailure(T3HookConfigError)
T3HookRunner-->>ClaudeAdapter: decision ask, title "T3 hook config failed"
else hooks.json valid
T3HookRunner->>HookCommand: run hook command
HookCommand-->>T3HookRunner: HookCommandOutput (allow/ask/deny)
T3HookRunner-->>ClaudeAdapter: normalizedDecision
end
end
ClaudeAdapter-->>User: tool allowed / approval prompt / denied
Loading

File-Level Changes

ChangeDetailsFiles
Make T3HookRunner re-resolve hooks config on every evaluation and expose live hook presence and fail-closed behavior.
  • Introduce HooksConfigEventKeys schema and SUPPORTED_HOOK_EVENTS to parse hook event keys from hooks.json.
  • Add warning deduping for unsupported hook events and emit Effect.logWarning with path and event details.
  • Add hasPreToolUseHooksNow effect to T3HookPlan and implement resolvePlanState helper to re-read config each time.
  • Update prepare to take an initial snapshot but have evaluatePreToolUse re-resolve config, handling T3HookConfigError by logging and returning an ask decision.
  • Add logConfigFailure helper to centralize config failure logging and reuse it in live evaluation paths.
apps/server/src/hooks/T3HookRunner.ts
Extend T3HookRunner tests to cover live config creation and edits, fail-closed behavior on invalid config, and unsupported-event warnings.
  • Add writeHooksConfig helper to create .t3code/hooks.json in a temp project directory.
  • Test that a config created after prepare toggles hasPreToolUseHooksNow to true and affects evaluation decisions.
  • Test that matcher edits are picked up without re-preparing, allowing previously matched calls when hooks no longer match.
  • Test that invalid JSON in hooks.json causes hasPreToolUseHooksNow to report hooks and evaluation to return an ask decision with a failure title and reason.
  • Test that configs declaring unsupported events (PostToolUse, Stop) log exactly one warning while still running PreToolUse hooks.
apps/server/src/hooks/T3HookRunner.test.ts
Wire CodexSessionRuntime to use spawn-time hook snapshot for initial thread open but recompute approval routing per turn from live hook presence.
  • Rename initial interceptApprovals flag to interceptApprovalsAtStart and keep using it only for openCodexThread.
  • On each sendTurn, derive interceptApprovals from options.hookPlan.hasPreToolUseHooksNow to reflect live config changes.
  • Ensure buildTurnStartParams receives the recomputed interceptApprovals for each turn.
  • Add a test that when a full-access turn has no T3 hooks, approvalPolicy is set to never while sandboxPolicy stays dangerFullAccess.
apps/server/src/provider/Layers/CodexSessionRuntime.ts
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Adjust Claude and Codex adapters and their tests to satisfy the expanded T3HookPlan interface with hasPreToolUseHooksNow.
  • Update ClaudeAdapter getHookPlan fallback plan to set hasPreToolUseHooksNow to Effect.succeed(false) when no hook runner is present.
  • Update ClaudeAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan stubs.
  • Update CodexAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan literals checked by identity.
  • Leave existing approval flows and evaluation behavior unchanged apart from the new field.
apps/server/src/provider/Layers/ClaudeAdapter.ts
apps/server/src/provider/Layers/ClaudeAdapter.test.ts
apps/server/src/provider/Layers/CodexAdapter.test.ts
Document live hook config behavior, supported events, and unreadable-config handling, and check in the execution plan for solidifying project hooks.
  • Extend permission-modes documentation with details on live hooks.json re-reads for Claude and Codex threads.
  • Describe that only PreToolUse is supported and that other events are logged once as warnings, not executed.
  • Explain fail-closed behavior when hooks.json becomes unreadable, turning tool calls into approval prompts.
  • Add .plans/solidify-project-hooks.md capturing the locked execution plan and rationale for the implementation.
docs/user/permission-modes.md
.plans/solidify-project-hooks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 13, 2026

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In readConfig, you decode the same JSON string twice (once via decodeHooksConfigJson and again via decodeHooksConfigEventKeysJson just to get event keys); consider deriving unsupportedEvents directly from config.hooks to avoid the extra parse and reduce complexity.
  • hasPreToolUseHooksNow currently uses a broad Effect.catch that logs a config failure and returns true for any error; narrowing this to T3HookConfigError (similar to the evaluatePreToolUse catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `readConfig`, you decode the same JSON string twice (once via `decodeHooksConfigJson` and again via `decodeHooksConfigEventKeysJson` just to get event keys); consider deriving `unsupportedEvents` directly from `config.hooks` to avoid the extra parse and reduce complexity.
-`hasPreToolUseHooksNow` currently uses a broad `Effect.catch` that logs a config failure and returns `true` for any error; narrowing this to `T3HookConfigError` (similar to the `evaluatePreToolUse` catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
## Individual Comments### Comment 1
<locationpath="apps/server/src/hooks/T3HookRunner.ts"line_range="384" />
<code_context>
return { decision: "allow" } satisfies T3HookDecision;
});
+ const resolvePlanState = Effect.fn("T3HookRunner.resolvePlanState")(function* (cwd: string) {
+ const configPathOption = yield* findConfigPath(cwd);
+ if (Option.isNone(configPathOption)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new hook plan flow by caching path-derived state, centralizing config-error handling, and deriving unsupported event keys from the already-decoded config instead of extra schemas.
- The extra indirection around config/state resolution and error handling is noticeable. You can keep the new live behaviors but simplify the flow and reduce repeated work.
### 1. Avoid re-running `findConfigPath` on every operation
You currently do:
```tsconst snapshot =yield*resolvePlanState(cwd);
// ...hasPreToolUseHooksNow: resolvePlanState(cwd).pipe(/* ... */),
evaluatePreToolUse: (input) =>Effect.gen(function* () {
const state =yield*resolvePlanState(cwd);
// ...
}).pipe(/* ... */),
````resolvePlanState` re-runs `findConfigPath(cwd)` every time, even though `cwd` is fixed per `prepare` call. You can keep live config re-reads but cache the `configPath` and `projectDirectory` once and re-use them:
```tsconst prepare:T3HookRunner["Service"]["prepare"] =Effect.fn("T3HookRunner.prepare")(function* (cwd) {
const configPathOption =yield*findConfigPath(cwd);
if (Option.isNone(configPathOption)) {
return {
configPath: undefined,
hasPreToolUseHooks: false,
hasPreToolUseHooksNow: Effect.succeed(false),
evaluatePreToolUse: () =>Effect.succeed({ decision: "allow"asconst }),
} satisfiesT3HookPlan;
}
const configPath =configPathOption.value;
const projectDirectory =path.dirname(path.dirname(configPath));
const readCurrentState =Effect.gen(function* () {
const config =yield*readConfig(configPath);
return {
configPath,
projectDirectory,
entries: config.hooks.PreToolUse?? ([] asReadonlyArray<HookMatcherConfig>),
};
});
const initialState =yield*readCurrentState;
const hasHooks =initialState.entries.length>0;
return {
configPath,
hasPreToolUseHooks: hasHooks,
hasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>logConfigFailure(error).pipe(Effect.as(true))),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
Effect.flatMap((state) =>state.entries.length===0?Effect.succeed({ decision: "allow" } satisfiesT3HookDecision)
:evaluateEntries({
entries: state.entries,
configPath: state.configPath,
projectDirectory: state.projectDirectory!,
payload: { ...input, cwd },
}),
),
Effect.catchTag(
"T3HookConfigError",
handleConfigFailure, // see helper below
),
),
} satisfiesT3HookPlan;
});
```
This keeps:
- Single `findConfigPath` per `prepare`.
- Live config re-reading via `readCurrentState`.
- Snapshot fields (`configPath`, `hasPreToolUseHooks`) plus dynamic ones, but with clearer boundaries.
### 2. Centralize error → decision mapping
Right now the mapping from `T3HookConfigError` to `T3HookDecision` is inlined inside `prepare`:
```tsEffect.catchTag("T3HookConfigError", (error) =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
),
),
```
Encapsulating this makes both `hasPreToolUseHooksNow` and `evaluatePreToolUse` easier to read:
```tsconst handleConfigFailure = (error:T3HookConfigError):Effect.Effect<T3HookDecision> =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
);
```
Then:
```tshasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>errorinstanceofT3HookConfigError?handleConfigFailure(error).pipe(Effect.map(() =>true))
:logConfigFailure(errorasT3HookConfigError).pipe(Effect.as(true)),
),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
// ...Effect.catchTag("T3HookConfigError", handleConfigFailure),
),
```
This keeps your UX behavior identical but removes duplicated decision construction and shortens the pipelines.
### 3. Simplify unsupported-event warning
You added a second schema + decoder just to get event keys:
```tsconst HooksConfigEventKeys =Schema.Struct({
hooks: Schema.Record(Schema.String, Schema.Unknown),
});
const decodeHooksConfigEventKeysJson =Schema.decodeUnknownEffect(
fromLenientJson(HooksConfigEventKeys),
);
const declaredEvents =yield*decodeHooksConfigEventKeysJson(raw).pipe(
Effect.map((decoded) =>Object.keys(decoded.hooks)),
Effect.orElseSucceed(() => [] asReadonlyArray<string>),
);
```
Since you already successfully decoded `config` above, you can derive event keys directly from it and drop the extra schema/decoder:
```tsconst config =yield*decodeHooksConfigJson(raw);
// ...const declaredEvents =Object.keys(config.hooks);
const unsupportedEvents =declaredEvents
.filter((event) =>!SUPPORTED_HOOK_EVENTS.includes(eventas (typeofSUPPORTED_HOOK_EVENTS)[number]))
.sort();
if (unsupportedEvents.length>0) {
if (!warnedUnsupportedEvents.has(configPath)) {
warnedUnsupportedEvents.add(configPath);
yield*Effect.logWarning("ignoring unsupported T3 hook events", {
path: configPath,
unsupportedEvents,
supportedEvents: SUPPORTED_HOOK_EVENTS,
});
}
}
```
If “warn once per config file” is sufficient, keying `warnedUnsupportedEvents` by `configPath` alone removes the need to build and track `warningKey` strings.
These changes keep all current behaviors (live config updates, fail-closed, warnings) but reduce indirection, duplicated logic, and statefulness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadapps/server/src/hooks/T3HookRunner.ts
Defmon3and others added 3 commits August 13, 2026 14:56
Re-resolve .t3code/hooks.json on every PreToolUse evaluation so mid-session
edits, creation, and deletion take effect without a restart; recompute Codex
approval routing per turn from the live config; fail closed (ask) with a
logged warning when the config becomes unreadable; warn once per config on
unsupported hook event keys instead of silently ignoring them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Defmon3
Defmon3force-pushed the feat/in-app-hook-confirmation branch from 6e304cb to 83d9fd8CompareAugust 13, 2026 13:05
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 83d9fd8.

This comment will update automatically after the next completed run.

@Defmon3
Defmon3 merged commit 75288e8 into customAug 13, 2026
8 of 12 checks passed
@Defmon3
Defmon3 deleted the feat/in-app-hook-confirmation branch August 13, 2026 13:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Defmon3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(server): in-app project hooks — live config, fail-closed, in-app approvals - #1

Merged
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation
Aug 13, 2026
Merged

feat(server): in-app project hooks — live config, fail-closed, in-app approvals#1
Defmon3 merged 3 commits into
customfrom
feat/in-app-hook-confirmation

Conversation

@Defmon3

@Defmon3Defmon3 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

What

Adds a Claude-hooks-style PreToolUse gate for Full access threads, configured per project via .t3code/hooks.json, with approvals surfaced in the app — then solidifies it:

  • Live config: .t3code/hooks.json is re-read before every hook check. Claude threads pick up create/edit/delete on the next tool call; Codex threads apply command/matcher edits on the next approval and recompute approval routing each turn.
  • Fail closed: an unreadable or invalid config turns tool calls into approval prompts (with a logged warning) instead of silently allowing.
  • No silent drops: unsupported hook event keys (PostToolUse, Stop, …) warn once in the server log.
  • Hook decisions: allow / ask / deny via stdout JSON or exit codes; Claude-compatible hookSpecificOutput accepted. Codex coverage maps command approvals → Bash, file changes → Edit.

Verification

  • vp test run over the 4 touched test files: 127/127 pass
  • vp run --filter t3 typecheck: clean in changed files
  • Two independent gpt-5.6-sol reviews over the rebased frozen scope; one low-severity warning-key collision was fixed and narrowly re-reviewed to zero surviving findings
  • Execution plan checked in at .plans/solidify-project-hooks.md; user docs updated in docs/user/permission-modes.md

🤖 Generated with Claude Code

Summary by Sourcery

Make T3 project hooks a live, fail-closed config for full-access sessions and align Codex approval routing with hook presence.

New Features:

  • Add live re-reading of .t3code/hooks.json before each PreToolUse hook evaluation so mid-session config changes take effect without restarting.
  • Expose a hasPreToolUseHooksNow signal on T3HookPlan for providers to detect current hook presence per turn.
  • Surface hook config failures as user-facing approval prompts instead of silently allowing tool calls.

Enhancements:

  • Warn once per config file about unsupported hook event keys while still running supported PreToolUse hooks.
  • Refine Codex full-access approval routing to stop requesting callbacks when a turn has no active project hooks.
  • Document project hook live-reload behavior, supported events, and unreadable-config handling in permission-modes user docs.
  • Add an internal execution plan document capturing the design and scope of the project-hooks solidification work.

Tests:

  • Extend T3HookRunner tests to cover live config creation/editing, fail-closed behavior on invalid configs, and unsupported-event warnings.
  • Adjust Codex and Claude adapter tests to account for the new hasPreToolUseHooksNow field and Codex approval routing behavior.
  • Add CodexSessionRuntime tests verifying approvalPolicy mapping when interceptApprovals is disabled in full-access mode.

@sourcery-ai

sourcery-aiBot commented Aug 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements live, fail-closed project hook behavior for PreToolUse across Claude and Codex runtimes, adds unsupported-event warnings, wires Codex approval routing to live hook presence, and documents the new behavior in user docs and an execution plan.

Sequence diagram for live PreToolUse evaluation and fail-closed behavior

sequenceDiagram
actor User
participant ClaudeAdapter
participant T3HookRunner
participant FileSystem
participant HookCommand
User->>ClaudeAdapter: invoke full-access tool
ClaudeAdapter->>T3HookRunner: prepare(cwd)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
T3HookRunner->>FileSystem: findConfigPath + read hooks.json
FileSystem-->>T3HookRunner: hooks.json content
T3HookRunner->>T3HookRunner: decodeHooksConfigJson
T3HookRunner->>T3HookRunner: decodeHooksConfigEventKeysJson
T3HookRunner->>T3HookRunner: logWarning on unsupported events
T3HookRunner-->>ClaudeAdapter: T3HookPlan(hasPreToolUseHooks, hasPreToolUseHooksNow)
loop each tool call
ClaudeAdapter->>T3HookRunner: plan.evaluatePreToolUse(input)
T3HookRunner->>T3HookRunner: resolvePlanState(cwd)
alt hooks.json unreadable/invalid
T3HookRunner->>T3HookRunner: logConfigFailure(T3HookConfigError)
T3HookRunner-->>ClaudeAdapter: decision ask, title "T3 hook config failed"
else hooks.json valid
T3HookRunner->>HookCommand: run hook command
HookCommand-->>T3HookRunner: HookCommandOutput (allow/ask/deny)
T3HookRunner-->>ClaudeAdapter: normalizedDecision
end
end
ClaudeAdapter-->>User: tool allowed / approval prompt / denied
Loading

File-Level Changes

ChangeDetailsFiles
Make T3HookRunner re-resolve hooks config on every evaluation and expose live hook presence and fail-closed behavior.
  • Introduce HooksConfigEventKeys schema and SUPPORTED_HOOK_EVENTS to parse hook event keys from hooks.json.
  • Add warning deduping for unsupported hook events and emit Effect.logWarning with path and event details.
  • Add hasPreToolUseHooksNow effect to T3HookPlan and implement resolvePlanState helper to re-read config each time.
  • Update prepare to take an initial snapshot but have evaluatePreToolUse re-resolve config, handling T3HookConfigError by logging and returning an ask decision.
  • Add logConfigFailure helper to centralize config failure logging and reuse it in live evaluation paths.
apps/server/src/hooks/T3HookRunner.ts
Extend T3HookRunner tests to cover live config creation and edits, fail-closed behavior on invalid config, and unsupported-event warnings.
  • Add writeHooksConfig helper to create .t3code/hooks.json in a temp project directory.
  • Test that a config created after prepare toggles hasPreToolUseHooksNow to true and affects evaluation decisions.
  • Test that matcher edits are picked up without re-preparing, allowing previously matched calls when hooks no longer match.
  • Test that invalid JSON in hooks.json causes hasPreToolUseHooksNow to report hooks and evaluation to return an ask decision with a failure title and reason.
  • Test that configs declaring unsupported events (PostToolUse, Stop) log exactly one warning while still running PreToolUse hooks.
apps/server/src/hooks/T3HookRunner.test.ts
Wire CodexSessionRuntime to use spawn-time hook snapshot for initial thread open but recompute approval routing per turn from live hook presence.
  • Rename initial interceptApprovals flag to interceptApprovalsAtStart and keep using it only for openCodexThread.
  • On each sendTurn, derive interceptApprovals from options.hookPlan.hasPreToolUseHooksNow to reflect live config changes.
  • Ensure buildTurnStartParams receives the recomputed interceptApprovals for each turn.
  • Add a test that when a full-access turn has no T3 hooks, approvalPolicy is set to never while sandboxPolicy stays dangerFullAccess.
apps/server/src/provider/Layers/CodexSessionRuntime.ts
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts
Adjust Claude and Codex adapters and their tests to satisfy the expanded T3HookPlan interface with hasPreToolUseHooksNow.
  • Update ClaudeAdapter getHookPlan fallback plan to set hasPreToolUseHooksNow to Effect.succeed(false) when no hook runner is present.
  • Update ClaudeAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan stubs.
  • Update CodexAdapter tests to include hasPreToolUseHooksNow: Effect.succeed(true) in hook plan literals checked by identity.
  • Leave existing approval flows and evaluation behavior unchanged apart from the new field.
apps/server/src/provider/Layers/ClaudeAdapter.ts
apps/server/src/provider/Layers/ClaudeAdapter.test.ts
apps/server/src/provider/Layers/CodexAdapter.test.ts
Document live hook config behavior, supported events, and unreadable-config handling, and check in the execution plan for solidifying project hooks.
  • Extend permission-modes documentation with details on live hooks.json re-reads for Claude and Codex threads.
  • Describe that only PreToolUse is supported and that other events are logged once as warnings, not executed.
  • Explain fail-closed behavior when hooks.json becomes unreadable, turning tool calls into approval prompts.
  • Add .plans/solidify-project-hooks.md capturing the locked execution plan and rationale for the implementation.
docs/user/permission-modes.md
.plans/solidify-project-hooks.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Aug 13, 2026

@sourcery-aisourcery-aiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In readConfig, you decode the same JSON string twice (once via decodeHooksConfigJson and again via decodeHooksConfigEventKeysJson just to get event keys); consider deriving unsupportedEvents directly from config.hooks to avoid the extra parse and reduce complexity.
  • hasPreToolUseHooksNow currently uses a broad Effect.catch that logs a config failure and returns true for any error; narrowing this to T3HookConfigError (similar to the evaluatePreToolUse catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments- In `readConfig`, you decode the same JSON string twice (once via `decodeHooksConfigJson` and again via `decodeHooksConfigEventKeysJson` just to get event keys); consider deriving `unsupportedEvents` directly from `config.hooks` to avoid the extra parse and reduce complexity.
-`hasPreToolUseHooksNow` currently uses a broad `Effect.catch` that logs a config failure and returns `true` for any error; narrowing this to `T3HookConfigError` (similar to the `evaluatePreToolUse` catchTag) would avoid mislabeling unrelated failures as hook-config issues while preserving the fail-closed behavior.
## Individual Comments### Comment 1
<locationpath="apps/server/src/hooks/T3HookRunner.ts"line_range="384" />
<code_context>
return { decision: "allow" } satisfies T3HookDecision;
});
+ const resolvePlanState = Effect.fn("T3HookRunner.resolvePlanState")(function* (cwd: string) {
+ const configPathOption = yield* findConfigPath(cwd);
+ if (Option.isNone(configPathOption)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new hook plan flow by caching path-derived state, centralizing config-error handling, and deriving unsupported event keys from the already-decoded config instead of extra schemas.
- The extra indirection around config/state resolution and error handling is noticeable. You can keep the new live behaviors but simplify the flow and reduce repeated work.
### 1. Avoid re-running `findConfigPath` on every operation
You currently do:
```tsconst snapshot =yield*resolvePlanState(cwd);
// ...hasPreToolUseHooksNow: resolvePlanState(cwd).pipe(/* ... */),
evaluatePreToolUse: (input) =>Effect.gen(function* () {
const state =yield*resolvePlanState(cwd);
// ...
}).pipe(/* ... */),
````resolvePlanState` re-runs `findConfigPath(cwd)` every time, even though `cwd` is fixed per `prepare` call. You can keep live config re-reads but cache the `configPath` and `projectDirectory` once and re-use them:
```tsconst prepare:T3HookRunner["Service"]["prepare"] =Effect.fn("T3HookRunner.prepare")(function* (cwd) {
const configPathOption =yield*findConfigPath(cwd);
if (Option.isNone(configPathOption)) {
return {
configPath: undefined,
hasPreToolUseHooks: false,
hasPreToolUseHooksNow: Effect.succeed(false),
evaluatePreToolUse: () =>Effect.succeed({ decision: "allow"asconst }),
} satisfiesT3HookPlan;
}
const configPath =configPathOption.value;
const projectDirectory =path.dirname(path.dirname(configPath));
const readCurrentState =Effect.gen(function* () {
const config =yield*readConfig(configPath);
return {
configPath,
projectDirectory,
entries: config.hooks.PreToolUse?? ([] asReadonlyArray<HookMatcherConfig>),
};
});
const initialState =yield*readCurrentState;
const hasHooks =initialState.entries.length>0;
return {
configPath,
hasPreToolUseHooks: hasHooks,
hasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>logConfigFailure(error).pipe(Effect.as(true))),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
Effect.flatMap((state) =>state.entries.length===0?Effect.succeed({ decision: "allow" } satisfiesT3HookDecision)
:evaluateEntries({
entries: state.entries,
configPath: state.configPath,
projectDirectory: state.projectDirectory!,
payload: { ...input, cwd },
}),
),
Effect.catchTag(
"T3HookConfigError",
handleConfigFailure, // see helper below
),
),
} satisfiesT3HookPlan;
});
```
This keeps:
- Single `findConfigPath` per `prepare`.
- Live config re-reading via `readCurrentState`.
- Snapshot fields (`configPath`, `hasPreToolUseHooks`) plus dynamic ones, but with clearer boundaries.
### 2. Centralize error → decision mapping
Right now the mapping from `T3HookConfigError` to `T3HookDecision` is inlined inside `prepare`:
```tsEffect.catchTag("T3HookConfigError", (error) =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
),
),
```
Encapsulating this makes both `hasPreToolUseHooksNow` and `evaluatePreToolUse` easier to read:
```tsconst handleConfigFailure = (error:T3HookConfigError):Effect.Effect<T3HookDecision> =>logConfigFailure(error).pipe(
Effect.as({
decision: "ask",
title: "T3 hook config failed",
reason: `T3 project hooks could not be loaded from ${error.configPath}.`,
} satisfiesT3HookDecision),
);
```
Then:
```tshasPreToolUseHooksNow: readCurrentState.pipe(
Effect.map((state) =>state.entries.length>0),
Effect.catch((error) =>errorinstanceofT3HookConfigError?handleConfigFailure(error).pipe(Effect.map(() =>true))
:logConfigFailure(errorasT3HookConfigError).pipe(Effect.as(true)),
),
),
evaluatePreToolUse: (input) =>readCurrentState.pipe(
// ...Effect.catchTag("T3HookConfigError", handleConfigFailure),
),
```
This keeps your UX behavior identical but removes duplicated decision construction and shortens the pipelines.
### 3. Simplify unsupported-event warning
You added a second schema + decoder just to get event keys:
```tsconst HooksConfigEventKeys =Schema.Struct({
hooks: Schema.Record(Schema.String, Schema.Unknown),
});
const decodeHooksConfigEventKeysJson =Schema.decodeUnknownEffect(
fromLenientJson(HooksConfigEventKeys),
);
const declaredEvents =yield*decodeHooksConfigEventKeysJson(raw).pipe(
Effect.map((decoded) =>Object.keys(decoded.hooks)),
Effect.orElseSucceed(() => [] asReadonlyArray<string>),
);
```
Since you already successfully decoded `config` above, you can derive event keys directly from it and drop the extra schema/decoder:
```tsconst config =yield*decodeHooksConfigJson(raw);
// ...const declaredEvents =Object.keys(config.hooks);
const unsupportedEvents =declaredEvents
.filter((event) =>!SUPPORTED_HOOK_EVENTS.includes(eventas (typeofSUPPORTED_HOOK_EVENTS)[number]))
.sort();
if (unsupportedEvents.length>0) {
if (!warnedUnsupportedEvents.has(configPath)) {
warnedUnsupportedEvents.add(configPath);
yield*Effect.logWarning("ignoring unsupported T3 hook events", {
path: configPath,
unsupportedEvents,
supportedEvents: SUPPORTED_HOOK_EVENTS,
});
}
}
```
If “warn once per config file” is sufficient, keying `warnedUnsupportedEvents` by `configPath` alone removes the need to build and track `warningKey` strings.
These changes keep all current behaviors (live config updates, fail-closed, warnings) but reduce indirection, duplicated logic, and statefulness.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment threadapps/server/src/hooks/T3HookRunner.ts
Defmon3and others added 3 commits August 13, 2026 14:56
Re-resolve .t3code/hooks.json on every PreToolUse evaluation so mid-session
edits, creation, and deletion take effect without a restart; recompute Codex
approval routing per turn from the live config; fail closed (ask) with a
logged warning when the config becomes unreadable; warn once per config on
unsupported hook event keys instead of silently ignoring them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Defmon3
Defmon3force-pushed the feat/in-app-hook-confirmation branch from 6e304cb to 83d9fd8CompareAugust 13, 2026 13:05
@github-actions

Copy link
Copy Markdown

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 83d9fd8.

This comment will update automatically after the next completed run.

@Defmon3
Defmon3 merged commit 75288e8 into customAug 13, 2026
8 of 12 checks passed
@Defmon3
Defmon3 deleted the feat/in-app-hook-confirmation branch August 13, 2026 13:24
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:Lvouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Defmon3