Skip to content

feat(kickoff): add --permission-mode <mode> for finer-grained claude control (#603)#608

Merged
dollspace-gay merged 2 commits into
developfrom
fix/603-claude-permission-mode
May 15, 2026
Merged

feat(kickoff): add --permission-mode <mode> for finer-grained claude control (#603)#608
dollspace-gay merged 2 commits into
developfrom
fix/603-claude-permission-mode

Conversation

@dollspace-gay
Copy link
Copy Markdown

@dollspace-gay dollspace-gay commented May 15, 2026

Summary

Fixes GH#603. crosslink kickoff run previously only exposed --skip-permissions, which always passed --dangerously-skip-permissions (full bypass) to the spawned claude session. That's appropriate for Docker-sandboxed kickoffs but more permissive than needed when running on the host (--container none). Claude itself supports a finer-grained --permission-mode <mode> flag — auto in particular keeps the permission classifier active for anomalous tool calls while routine ones go through, which is what users typically want for unattended host runs.

This PR adds --permission-mode <mode> to kickoff run and kickoff launch, mutually exclusive with --skip-permissions, validated at clap parse time against claude's known set.

Choice of approach

The issue offers two paths — a CLI flag (option A) or an env var (option B). Going with A for the cleaner long-term surface: validation happens at parse time, the option is discoverable via --help, and it composes correctly with conflicts_with against the legacy --skip-permissions.

Usage

# Default (unchanged):
crosslink kickoff run "feature" --container none --skip-permissions

# New: auto mode — classifier active, no human approval needed:
crosslink kickoff run "feature" --container none --permission-mode auto

# Mutually exclusive — clap rejects this at parse time:
crosslink kickoff run "feature" --skip-permissions --permission-mode auto
# error: the argument '--skip-permissions' cannot be used with '--permission-mode <permission_mode>'

Valid <mode> values (enforced by clap::builder::PossibleValuesParser):
acceptEdits, auto, bypassPermissions, default, dontAsk, plan

Threading

The flag walks down through every kickoff dispatch path:

  • main.rsKickoffCommands::{Run, Launch} gain the field. The fallthrough Launch default in the bare crosslink kickoff interactive wizard also gets permission_mode: None.
  • types.rs::KickoffOpts — new permission_mode: Option<&'a str> field.
  • mod.rs — destructured in both Run and Launch match arms, threaded through dispatch_launch's signature, set in all three KickoffOpts construction sites (direct Run, dispatch_launch's --run path, and the wizard's WizardStage::Run path).
  • run.rs — passes opts.permission_mode to launch_local.
  • launch.rslaunch_local signature gains the param; build_agent_command does the resolution (see below).
  • plan.rs — plan mode passes None (plan mode never overrides permissions).
  • swarm/lifecycle.rs, sentinel/engine.rs — internal KickoffOpts builders updated with permission_mode: None (not user-facing).

Container mode (launch_container) does not use build_agent_command — it relies on the agent image's own entrypoint — so no plumbing there.

Resolution semantics

Inside build_agent_command:

match (permission_mode, skip_permissions) {
    (Some(mode), _) if !mode.is_empty() => format!(" --permission-mode {}", shell_escape_arg(mode)),
    (_, true)                           => " --dangerously-skip-permissions".to_string(),
    _                                   => String::new(),
}
  • permission_mode (when non-empty) takes precedence over skip_permissions.
  • Empty-string permission_mode is treated as None and falls back to the legacy resolution.
  • CLI parsing makes the Some(...) + true case unreachable from the public surface (conflicts_with), but the defense-in-depth ordering is tested anyway for internal callers.

Test plan

  • cargo test --lib --bin crosslink — 2878 passed (+3 new)
  • cargo clippy -- -D warnings -W clippy::unwrap_used -W clippy::expect_used — clean
  • cargo fmt --all -- --check — clean
  • New tests:
    • test_build_agent_command_with_permission_mode_auto — emits --permission-mode 'auto', no --dangerously-skip-permissions
    • test_build_agent_command_permission_mode_wins_over_skip_permissions — defense-in-depth precedence check
    • test_build_agent_command_empty_permission_mode_treated_as_none — empty string falls back to legacy resolution
  • Existing 11 build_agent_command test calls + 14 KickoffOpts test constructions updated for the new field
  • Manual repro from the issue:
    crosslink kickoff run "feature" --doc design.md --issue 1 \
        --container none --timeout 24h --permission-mode auto
    Tmux pane should show claude's command line includes --permission-mode 'auto' and skip the bypass-permissions warning prompt.

Out of scope

  • Adding an env var fallback. Could be tacked on later as CROSSLINK_PERMISSION_MODE for scripts/CI that don't want to rewrite CLI invocations, but keeping the surface area minimal here.
  • Threading permission_mode into launch_container. The container path uses the agent image's own permission setup, not build_agent_command.

Cross-refs

🤖 Generated with Claude Code

…control (#603)

Previously, `crosslink kickoff run` only exposed `--skip-permissions`,
which always passed `--dangerously-skip-permissions` (full bypass) to
the spawned claude session. This is appropriate for Docker-sandboxed
kickoffs but more permissive than needed when running on the host
(`--container none`). Claude itself supports a finer-grained
`--permission-mode <mode>` flag (acceptEdits, auto, bypassPermissions,
default, dontAsk, plan); `auto` in particular is what users want for
unattended host runs — the permission classifier stays active for
anomalous tool calls.

This commit adds `--permission-mode <mode>` to `kickoff run` and
`kickoff launch`, mutually exclusive with `--skip-permissions`,
validated at clap parse time against claude's known set.

Threading:
- main.rs: KickoffCommands::{Run,Launch} gain the flag with
  `PossibleValuesParser` validation and `conflicts_with` for
  `skip_permissions`. The fallthrough `Launch` default in the bare
  `crosslink kickoff` wizard also gets `permission_mode: None`.
- types.rs: KickoffOpts.permission_mode: Option<&str>
- mod.rs: destructured in Run + Launch match arms, plumbed through
  dispatch_launch's signature, set in all three KickoffOpts
  construction sites (direct Run, dispatch_launch's --run path,
  wizard WizardStage::Run path)
- run.rs: passes through to launch_local
- launch.rs: launch_local signature gains `permission_mode`, forwards
  to build_agent_command. Resolution: permission_mode (when non-empty)
  wins over skip_permissions; empty string falls back to
  skip_permissions; neither = no flag.
- plan.rs: plan mode passes `None` (plan mode never overrides perms).
- swarm/lifecycle.rs, sentinel/engine.rs: KickoffOpts builders updated
  with `permission_mode: None` (internal callers, not user-facing).

Usage:

    crosslink kickoff run "feature" --container none --permission-mode auto

Tests: 2878 pass (+3 new):
- test_build_agent_command_with_permission_mode_auto
- test_build_agent_command_permission_mode_wins_over_skip_permissions
- test_build_agent_command_empty_permission_mode_treated_as_none

Existing 11 build_agent_command test calls + 14 KickoffOpts test
constructions updated for the new field. Clippy `-D warnings -W
clippy::unwrap_used -W clippy::expect_used` clean, fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h_local

I added the attribute when extending `launch_local`'s signature for
`permission_mode` (#603), but the original was already there. CI clippy
catches it via `clippy::duplicated_attributes` (newly part of -D
warnings on the CI rustc); my local rustc didn't flag it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dollspace-gay dollspace-gay merged commit 2cab4b1 into develop May 15, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kickoff: expose claude --permission-mode (auto/acceptEdits/…) so unsandboxed agents don't have to use --dangerously-skip-permissions

1 participant