Skip to content

fix(cli): preserve permission mode after ExitPlanMode + sidechain UUID chain fix#658

Open
yrom wants to merge 3 commits into
tiann:mainfrom
yrom:fix/permission-mode-exitplan
Open

fix(cli): preserve permission mode after ExitPlanMode + sidechain UUID chain fix#658
yrom wants to merge 3 commits into
tiann:mainfrom
yrom:fix/permission-mode-exitplan

Conversation

@yrom
Copy link
Copy Markdown

@yrom yrom commented May 21, 2026

Summary

  • Preserve permission mode after ExitPlanMode approval: When ExitPlanMode is approved, use the current permissionMode as fallback instead of hardcoded 'default', and honor explicit mode selections from the approval response.
  • Prevent sidechain UUID chain break from invisible system init messages: System init/result messages updated sidechainLastUUID but were skipped by the tracer's normalizer, orphaning all subsequent subagent messages including permission request cards in the web UI. Fixes [BUG] Permission request fails to show for sub-agent runs in Claude Code #587.

Test plan

  • permissionHandler.test.ts — 4 tests (ExitPlanMode default mode preservation + YOLO plan mode)
  • sdkToLogConverter.test.ts — 2 tests for sidechain UUID chain with system init messages
  • All tests pass via cd cli && bunx vitest run src/claude/utils/permissionHandler.test.ts src/claude/utils/sdkToLogConverter.test.ts

yrom added 2 commits May 21, 2026 23:13
When ExitPlanMode is approved, use the current permissionMode as
fallback instead of hardcoded 'default', and honor explicit mode
selections from the approval response.
…it messages

System init/result messages updated sidechainLastUUID but were skipped
by the tracer's normalizer, orphaning all subsequent subagent messages
including permission request cards in the web UI.

Fixes tienn/hapi#587
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] Consume auto-approved tool calls before name-only matching — resolveToolCallId() now takes the first unused call with the same tool name. Auto-approved paths above it (allowedTools, Bash allow rules, bypassPermissions, acceptEdits) return before calling the resolver, so those tool calls remain used: false until their later tool_result arrives. If Claude emits multiple same-name tools in one turn and an earlier one is auto-approved while a later one needs approval, the later permission request can be attached to the earlier tool id, which releases/records permissions against the wrong tool card and can abort/log the wrong result. Evidence: cli/src/claude/utils/permissionHandler.ts:414.
    Suggested fix:
    private consumeToolCallId(name: string, input: unknown): void {
        this.resolveToolCallId(name, input);
    }
    
    // Before every early allow return, consume the emitted tool_use id too.
    if (this.allowedBashLiterals.has(inputObj.command)) {
        this.consumeToolCallId(toolName, input);
        return { behavior: 'allow', updatedInput: input as Record<string, unknown> };
    }
    Apply the same consumption before the other early allow returns, then add a regression test where the first Bash call is allowed by allowTools/prefix and the second same-name Bash call requires approval.

Summary
Review mode: initial
One major issue found in the permission resolver change. Sidechain UUID handling looked consistent with the web normalizer/tracer behavior in surrounding code.

Testing
Not run (automation/security: review-only; did not execute PR code).

HAPI Bot

Comment thread cli/src/claude/utils/permissionHandler.ts Outdated
@yrom yrom force-pushed the fix/permission-mode-exitplan branch from 81826bb to ecfbd74 Compare May 21, 2026 15:14
@yrom yrom changed the title fix(cli): preserve permission mode + sidechain UUID chain fix fix(cli): preserve permission mode after ExitPlanMode + sidechain UUID chain fix May 21, 2026
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] ExitPlanMode approval can restart Claude back in plan mode — normal approval sends no mode, so when the active mode is plan, the new fallback requeues PLAN_FAKE_RESTART with { permissionMode: 'plan' } instead of leaving plan mode. That can loop the approved plan back into planning instead of executing it. Evidence: cli/src/claude/utils/permissionHandler.ts:262.
    Suggested fix:
    const nextMode = response.mode && PLAN_EXIT_MODES.includes(response.mode)
        ? response.mode
        : this.permissionMode === 'plan'
            ? 'default'
            : this.permissionMode;
    this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: nextMode });

Summary
Review mode: follow-up after new commits
One major issue found in the follow-up diff. The previous bot finding about same-name tool call resolution appears addressed in the current head because resolveToolCallId() is back to input-aware matching.

Testing
Not run (automation/security: review-only; did not execute PR code). Missing regression coverage for approving ExitPlanMode while current permission mode is plan.

HAPI Bot

}
const nextMode = response.mode && PLAN_EXIT_MODES.includes(response.mode)
? response.mode
: this.permissionMode;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MAJOR] In the normal ExitPlanMode approval path the web/Telegram/voice callers approve without a mode, so while the session is actually in plan mode this fallback uses this.permissionMode === 'plan' and requeues PLAN_FAKE_RESTART with { permissionMode: 'plan' }. That restarts Claude in planning mode again instead of executing the approved plan.

Suggested fix:

const nextMode = response.mode && PLAN_EXIT_MODES.includes(response.mode)
    ? response.mode
    : this.permissionMode === 'plan'
        ? 'default'
        : this.permissionMode;
this.session.queue.unshift(PLAN_FAKE_RESTART, { permissionMode: nextMode });

Add a regression test for approving ExitPlanMode with current mode plan and no response mode.

When approving ExitPlanMode without an explicit mode response, falling
back to the current permissionMode would requeue with 'plan', causing
the approved plan to loop back into planning instead of executing.
Also fixes TS type errors in the test file that caused CI typecheck
to fail.
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] Sidechain still orphans after visible system events — api_error/turn-duration/compact system records are not skipped by isClaudeChatVisibleMessage, but normalizeAgentRecord() converts them to role: 'event' without UUID-bearing agent content, and traceMessages() only registers UUIDs from sidechain agent messages. Advancing sidechainLastUUID to the system event UUID therefore makes the next sidechain assistant point at a UUID the tracer never stores. Evidence: cli/src/claude/utils/sdkToLogConverter.ts:161, related context web/src/chat/normalizeAgent.ts:451 and web/src/chat/tracer.ts:73.
    Suggested fix:
    const shouldAdvanceSidechainChain = sdkMessage.type !== 'system';
    if (shouldAdvanceSidechainChain) {
        this.sidechainLastUUID.set((sdkMessage as any).parent_tool_use_id!, uuid);
    }

Summary
Review mode: follow-up after new commits
One major issue remains in the current sidechain UUID-chain fix. The prior ExitPlanMode finding is addressed in this head.

Testing
Not run (automation/security: review-only; bun is not installed in this runner).

HAPI Bot

// the tracer can never resolve, orphaning the entire sidechain.
const isInvisibleToTracer = sdkMessage.type === 'system'
&& ((sdkMessage as SDKSystemMessage).subtype === 'init' || (sdkMessage as SDKSystemMessage).subtype === 'result');
if (!isInvisibleToTracer) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MAJOR] This still advances the sidechain parent map to system records that the tracer cannot resolve. api_error/turn-duration/compact system records are normalized as role: 'event' without UUID-bearing agent content, and traceMessages() only stores UUIDs from sidechain agent messages, so the next sidechain assistant becomes an orphan when its parentUuid is this system UUID.

Suggested fix:

const shouldAdvanceSidechainChain = sdkMessage.type !== 'system';
if (shouldAdvanceSidechainChain) {
    this.sidechainLastUUID.set((sdkMessage as any).parent_tool_use_id!, uuid);
}

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Permission request fails to show for sub-agent runs in Claude Code

1 participant