Skip to content

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

@maxine-at-forecast

Description

@maxine-at-forecast

For crosslink kickoff run --container none (or any local-process kickoff), the only way to run a claude agent unattended is to pass --skip-permissions, which causes the spawned claude session to use --dangerously-skip-permissions (full bypass). This is appropriate for Docker-sandboxed kickoffs but is more permissive than needed in --container none mode where the agent is running directly on the host's user account.

claude itself (v2.1.118) supports a finer-grained --permission-mode <mode> flag with choices: acceptEdits, auto, bypassPermissions, default, dontAsk, plan. Auto mode in particular keeps the permission classifier active and is what users typically want for unattended runs on the host: routine tool calls go through, anomalous ones still get gated.

crosslink kickoff doesn't expose any of these — only the boolean --skip-permissions which always emits --dangerously-skip-permissions.

Repro

crosslink kickoff run \"some feature\" --doc design.md --issue 1 --container none --timeout 24h --skip-permissions

→ in the tmux pane, the agent shows the "WARNING: Claude Code running in Bypass Permissions mode" prompt requiring a human keystroke to dismiss, then proceeds with full bypass. With --permission-mode auto, neither happens — claude starts in auto mode directly, with the classifier active.

Two ways to fix

(A) New CLI flag — --permission-mode <mode> on kickoff run (and kickoff launch, kickoff plan), mutually exclusive with --skip-permissions. Cleanest API; passes through to claude's --permission-mode. Threads through types.rs::RunOptions, mod.rs, run.rs, launch.rs::build_agent_command, ~13 test cases in tests.rs. Larger surface change.

(B) Opt-in env var — CROSSLINK_PERMISSION_MODE that overrides the skip-permissions resolution in build_agent_command. Single-hunk change in launch.rs, default behavior preserved, no test changes, no CLI signature change.

I've been running (B) locally for the last few hours and it works well — the diff below is exactly what's deployed. Happy with either approach; (B) is what I'd file as a PR if I were the one shipping it, since it's surgical and doesn't change the CLI surface. (A) is cleaner long-term once the surface is going to grow anyway.

Minimal patch (option B)

diff --git a/crosslink/src/commands/kickoff/launch.rs b/crosslink/src/commands/kickoff/launch.rs
@@ -167,11 +167,25 @@ pub(super) fn build_agent_command(
 ) -> String {
     use crate::utils::shell_escape_arg;
 
-    let skip_flag = if skip_permissions {
-        \" --dangerously-skip-permissions\"
-    } else {
-        \"\"
+    // Resolve permission posture for the spawned claude session:
+    //   1. CROSSLINK_PERMISSION_MODE env var, if set and non-empty, overrides
+    //      everything and emits \`--permission-mode <value>\` (claude supports
+    //      acceptEdits, auto, bypassPermissions, default, dontAsk, plan).
+    //   2. Otherwise, if the caller passed --skip-permissions, emit the
+    //      \`--dangerously-skip-permissions\` legacy flag (full bypass).
+    //   3. Otherwise no flag — claude prompts for every tool.
+    // Use case for #1: launching with \`--container none\` (no Docker sandbox)
+    // where you want claude's auto-mode classifier active rather than a full
+    // bypass.
+    let env_permission_mode = std::env::var(\"CROSSLINK_PERMISSION_MODE\")
+        .ok()
+        .filter(|v| !v.is_empty());
+    let skip_flag_owned = match (env_permission_mode, skip_permissions) {
+        (Some(mode), _) => format!(\" --permission-mode {}\", shell_escape_arg(&mode)),
+        (None, true) => \" --dangerously-skip-permissions\".to_string(),
+        (None, false) => String::new(),
     };
+    let skip_flag = skip_flag_owned.as_str();

Usage after applying:

# Default behavior preserved — full bypass:
crosslink kickoff run \"...\" --container none --skip-permissions

# New: auto mode (or any other claude permission mode):
CROSSLINK_PERMISSION_MODE=auto crosslink kickoff run \"...\" --container none --skip-permissions

(--skip-permissions is still passed so the rest of the launch.rs logic doesn't bail; the env var override just rewrites which flag claude actually gets.)

Validation tests on the local build

  • cargo install --path crosslink --locked → clean rebuild
  • Existing test test_build_agent_command_with_skip_permissions still passes (env var unset, --skip-permissions=true → emits --dangerously-skip-permissions)
  • Manual: CROSSLINK_PERMISSION_MODE=auto crosslink kickoff run \"...\" --container none --skip-permissions → tmux pane shows the agent's claude command-line includes --permission-mode 'auto' and skips the bypass-permissions warning prompt entirely; "⏵⏵ auto mode on" visible

Cross-refs

This is a small, well-scoped opt-in. Happy to refine the env var name (CLAUDE_PERMISSION_MODE? CROSSLINK_AGENT_PERMISSION_MODE?) or convert to a CLI flag if you'd rather take (A) — just let me know.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions