Skip to content

fix(hub,cli): forward permissionMode on session resume#460

Open
junmo-kim wants to merge 4 commits intotiann:mainfrom
junmo-kim:fix/resume-permission-mode
Open

fix(hub,cli): forward permissionMode on session resume#460
junmo-kim wants to merge 4 commits intotiann:mainfrom
junmo-kim:fix/resume-permission-mode

Conversation

@junmo-kim
Copy link
Copy Markdown
Contributor

Problem

When a session is resumed, the permission mode resets to default regardless of what mode was active before. For example, a session running in bypassPermissions (yolo) mode loses that setting on resume.

Solution

Forward the cached permissionMode through the Hub → Runner → CLI pipeline on resume via a new --permission-mode flag.

  • Hub: resumeSession() reads session.permissionMode from the in-memory cache and passes it to spawnSession()
  • Runner: converts to --permission-mode <mode> CLI argument (validated against PERMISSION_MODES)
  • CLI: each flavor command parses and validates against its own allowed modes (e.g. CLAUDE_PERMISSION_MODES)
  • Backward compat: --yolo is preserved as a shorthand; --permission-mode takes precedence when both are present

Open question: DB persistence

Currently permissionMode lives only in the Hub's in-memory session cache (populated via keepalive). This means it survives session resume but not Hub restarts — same as collaborationMode today.

By contrast, model and effort are persisted to the sessions table (schema v5/v6) and survive Hub restarts.

Would you prefer a follow-up to add permission_mode to the DB (schema v7 migration), consistent with how model/effort are handled? Or is in-memory sufficient given that Hub restarts are relatively rare?

Tests

  • Added test in sessionModel.test.ts: sets permissionMode via keepalive → ends session → resumes → verifies permissionMode is forwarded to spawnSession()
  • All 60 hub tests pass

When a session is resumed, the cached permissionMode is now forwarded
through the Hub → Runner → CLI pipeline via a new --permission-mode
flag. Previously the mode was lost on resume, resetting to 'default'.

Each CLI flavor validates the flag value against its own allowed
permission modes (e.g. CLAUDE_PERMISSION_MODES) and rejects unknown
values. The existing --yolo flag is preserved as a shorthand.
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

  • No actionable issues found in the added/modified lines.

Summary

  • Review mode: initial. No actionable issues found in the latest diff. Residual risk: coverage only exercises hub-side forwarding for a Claude session; there is still no CLI parser or end-to-end resume coverage for the other agents touched here.
  • Not found in repo/docs: tests covering the new --permission-mode parsing in cli/src/commands/*.

Testing

  • Not run (automation). bun is not available in this runner image, so bun test hub/src/sync/sessionModel.test.ts could not be executed.

HAPI Bot

Extract the CLI argument construction logic into a standalone
exported function so it can be unit-tested independently.
No behavior change.
Verify that the runner correctly forwards valid permission modes
via --permission-mode, rejects invalid values, and falls back to
--yolo when no permission mode is set.
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] --permission-mode does not actually take precedence over the legacy --yolo flag in the new CLI parsers. The runner helper emits only one flag (cli/src/runner/run.ts:860), but the direct entrypoints still overwrite options.permissionMode when --yolo appears later, so commands like hapi --permission-mode default --yolo or hapi codex resume <id> --permission-mode read-only --yolo resume in the more permissive mode instead of the explicit override. Evidence: cli/src/commands/claude.ts:40, cli/src/commands/codex.ts:54, cli/src/commands/cursor.ts:38, cli/src/commands/gemini.ts:33, cli/src/commands/opencode.ts:32.
    Suggested fix:
    let explicitPermissionMode: PermissionMode | undefined
    
    } else if (arg === '--permission-mode') {
        // validate mode
        explicitPermissionMode = parsedMode
        options.permissionMode = parsedMode
    } else if ((arg === '--yolo' || arg === LEGACY_ALIAS) && explicitPermissionMode === undefined) {
        options.permissionMode = LEGACY_MODE
    }

Summary

  • Review mode: follow-up after new commits.
  • 1 actionable issue found in the latest diff: the new CLI parsers do not enforce the advertised --permission-mode precedence over --yolo, which can resume a session with broader permissions than the explicit flag requested.

Testing

  • Not run (automation). bun is not available in this runner image, so bun test cli/src/runner/buildCliArgs.test.ts hub/src/sync/sessionModel.test.ts could not be executed.

HAPI Bot

throw new Error(`Invalid --permission-mode value: ${mode ?? '(missing)'}`)
}
options.permissionMode = mode as StartOptions['permissionMode']
} else if (arg === '--yolo') {
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] --permission-mode still loses to a later --yolo, so the precedence promised by this PR is not actually enforced. The runner helper now emits only one flag, but direct CLI invocations like hapi --permission-mode default --yolo or hapi codex resume <id> --permission-mode read-only --yolo will still end up in the legacy yolo mode because this branch overwrites the explicit selection. The same pattern exists in cli/src/commands/codex.ts:60, cli/src/commands/cursor.ts:44, cli/src/commands/gemini.ts:39, and cli/src/commands/opencode.ts:38.

Suggested fix:

let explicitPermissionMode: StartOptions['permissionMode'] | undefined

} else if (arg === '--permission-mode') {
    // validate mode
    explicitPermissionMode = mode as StartOptions['permissionMode']
    options.permissionMode = explicitPermissionMode
} else if ((arg === '--yolo' || arg === '--dangerously-skip-permissions') && explicitPermissionMode === undefined) {
    options.permissionMode = 'bypassPermissions'
    unknownArgs.push('--dangerously-skip-permissions')
}

When both flags are present, --permission-mode was silently
overwritten by a later --yolo. Guard legacy flag branches with
a hasExplicitPermissionMode check so the explicit flag wins.
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] The new resume path still drops permissionMode after a hub restart. hub/src/sync/syncEngine.ts:444 now forwards session.permissionMode, but that field is only rehydrated from the live cache in hub/src/sync/sessionCache.ts:143, and the sessions table still has no persisted permission_mode column in hub/src/store/index.ts:179. After any hub restart, resumed sessions will still come back in default.

Summary

  • Review mode: follow-up after new commits.
  • 1 actionable issue found in the latest diff: resumed sessions only retain permissionMode until the hub process restarts.

Testing

  • Not run (security review; PR code treated as untrusted). The added test covers only the hot-cache path, not a store reload / hub restart path.

HAPI Bot

resumeToken,
session.effort ?? undefined
session.effort ?? undefined,
session.permissionMode ?? undefined
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 fixes the hot-cache resume path, but it still drops permissionMode after a hub restart. resumeSession() now forwards session.permissionMode, yet SessionCache.refreshSession() rehydrates that field from existing?.permissionMode only, and the sessions table still does not persist it. Once the hub process restarts, session.permissionMode becomes undefined again and the original bug comes back on the next resume.

Suggested fix:

// persist permissionMode the same way model/effort are persisted
if (payload.permissionMode !== undefined && payload.permissionMode !== session.permissionMode) {
    this.store.sessions.setSessionPermissionMode(payload.sid, payload.permissionMode, session.namespace, {
        touchUpdatedAt: false
    })
}

const session: Session = {
    // ...
    permissionMode: stored.permissionMode ?? existing?.permissionMode
}

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.

1 participant