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.
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 spawnedclaudesession to use--dangerously-skip-permissions(full bypass). This is appropriate for Docker-sandboxed kickoffs but is more permissive than needed in--container nonemode 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 kickoffdoesn't expose any of these — only the boolean--skip-permissionswhich always emits--dangerously-skip-permissions.Repro
→ 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>onkickoff run(andkickoff launch,kickoff plan), mutually exclusive with--skip-permissions. Cleanest API; passes through to claude's--permission-mode. Threads throughtypes.rs::RunOptions,mod.rs,run.rs,launch.rs::build_agent_command, ~13 test cases intests.rs. Larger surface change.(B) Opt-in env var —
CROSSLINK_PERMISSION_MODEthat overrides the skip-permissions resolution inbuild_agent_command. Single-hunk change inlaunch.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)
Usage after applying:
(
--skip-permissionsis 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 rebuildtest_build_agent_command_with_skip_permissionsstill passes (env var unset, --skip-permissions=true → emits --dangerously-skip-permissions)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" visibleCross-refs
--docdesign file #580 — kickoff macOS Keychain auth gap (closed; santana workaround in production).gitpointer unresolvable inside container + allowedTools blocks language build tools #584 — kickoff git-shim + allowedTools (closed; santana workaround in production)/designskill prelude zsh nomatch + ls non-zero (open)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.