From 539c9b857216ae598cf854751cf0b11d0569d97b Mon Sep 17 00:00:00 2001 From: Nik V Date: Tue, 28 Apr 2026 23:11:54 +0700 Subject: [PATCH 1/4] Respect Claude permission mode before adding bypass - Detect explicit Claude permission-mode arguments and avoid adding duplicate bypass flags - Read trusted Claude settings for defaultMode auto and disableAutoMode - Reuse one permission-argument builder across native and Docker Claude launches This lets Auto Mode run through cco when the user has selected it while keeping the existing bypass default otherwise. --- cco | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 115 insertions(+), 10 deletions(-) diff --git a/cco b/cco index 3d1829b..c928067 100755 --- a/cco +++ b/cco @@ -268,6 +268,111 @@ get_command() { fi } +claude_args_include_permission_mode() { + local arg + + if ! declare -p claude_args >/dev/null 2>&1; then + return 1 + fi + + for arg in "${claude_args[@]}"; do + case "$arg" in + --permission-mode | --permission-mode=* | --dangerously-skip-permissions | --allow-dangerously-skip-permissions) + return 0 + ;; + esac + done + + return 1 +} + +read_claude_permission_setting() { + local settings_file="$1" + local setting_name="$2" + + if [[ ! -f "$settings_file" ]]; then + return 1 + fi + + if command -v python3 >/dev/null 2>&1; then + python3 - "$settings_file" "$setting_name" <<'PY' 2>/dev/null +import json +import sys + +try: + with open(sys.argv[1], "r", encoding="utf-8") as handle: + settings = json.load(handle) +except Exception: + sys.exit(1) + +permissions = settings.get("permissions") +if not isinstance(permissions, dict): + sys.exit(1) + +value = permissions.get(sys.argv[2]) +if isinstance(value, str): + print(value) + sys.exit(0) + +sys.exit(1) +PY + return + fi + + if command -v jq >/dev/null 2>&1; then + jq -er --arg name "$setting_name" '.permissions[$name] // empty | strings' "$settings_file" 2>/dev/null + return + fi + + return 1 +} + +trusted_claude_settings_paths() { + printf '%s\n' "$PWD/.claude/settings.local.json" + printf '%s\n' "$(find_claude_config_dir)/settings.json" +} + +claude_auto_mode_disabled() { + local settings_file value + + while IFS= read -r settings_file; do + value=$(read_claude_permission_setting "$settings_file" "disableAutoMode" || true) + if [[ "$value" == "disable" ]]; then + return 0 + fi + done < <(trusted_claude_settings_paths) + + return 1 +} + +claude_default_permission_mode() { + local settings_file value + + while IFS= read -r settings_file; do + value=$(read_claude_permission_setting "$settings_file" "defaultMode" || true) + if [[ -n "$value" ]]; then + printf '%s\n' "$value" + return 0 + fi + done < <(trusted_claude_settings_paths) + + return 1 +} + +build_claude_permission_args() { + claude_permission_args=() + + if claude_args_include_permission_mode; then + return + fi + + if ! claude_auto_mode_disabled && [[ "$(claude_default_permission_mode || true)" == "auto" ]]; then + return + fi + + claude_permission_args=("--dangerously-skip-permissions") +} + # Load agent-specific behavior/policies from dedicated modules. if [[ -f "$CCO_INSTALLATION_DIR/lib/agents.sh" ]]; then # shellcheck source=/dev/null @@ -1748,12 +1853,9 @@ run_native_sandbox() { cmd+=("${cmd_array[@]}" "${claude_args[@]}") fi else - # Running Claude - use --dangerously-skip-permissions since we're in a sandbox - if [[ ${#claude_args[@]} -eq 0 ]]; then - cmd+=("${cmd_array[@]}" "--dangerously-skip-permissions") - else - cmd+=("${cmd_array[@]}" "--dangerously-skip-permissions" "${claude_args[@]}") - fi + # Running Claude - use cco's default permission mode unless the user configured one. + build_claude_permission_args + cmd+=("${cmd_array[@]}" "${claude_permission_args[@]}" "${claude_args[@]}") fi fi @@ -2525,7 +2627,8 @@ run_container() { if [[ -n "$command_flag" || -n "$CCO_COMMAND" ]]; then docker "${exec_args[@]}" "${cmd_array[@]}" "${claude_args[@]}" else - docker "${exec_args[@]}" "${cmd_array[@]}" --dangerously-skip-permissions "${claude_args[@]}" + build_claude_permission_args + docker "${exec_args[@]}" "${cmd_array[@]}" "${claude_permission_args[@]}" "${claude_args[@]}" fi fi local exec_status=$? @@ -2600,11 +2703,12 @@ run_container() { docker run "${docker_args[@]}" "$IMAGE_NAME" "${cmd_array[@]}" "${claude_args[@]}" fi else - # Running Claude - use --dangerously-skip-permissions + # Running Claude - use cco's default permission mode unless the user configured one. + build_claude_permission_args if [[ -n "$tty_flag" ]]; then - docker run "$tty_flag" "${docker_args[@]}" "$IMAGE_NAME" "${cmd_array[@]}" --dangerously-skip-permissions "${claude_args[@]}" + docker run "$tty_flag" "${docker_args[@]}" "$IMAGE_NAME" "${cmd_array[@]}" "${claude_permission_args[@]}" "${claude_args[@]}" else - docker run "${docker_args[@]}" "$IMAGE_NAME" "${cmd_array[@]}" --dangerously-skip-permissions "${claude_args[@]}" + docker run "${docker_args[@]}" "$IMAGE_NAME" "${cmd_array[@]}" "${claude_permission_args[@]}" "${claude_args[@]}" fi fi run_status=$? @@ -2763,6 +2867,7 @@ restore_credentials() { # Initialize variables claude_args=() +claude_permission_args=() additional_dirs=() additional_ro_paths=() deny_paths=() From 792a0939e235a32c4360c2e58418fa2373a04eae Mon Sep 17 00:00:00 2001 From: Nik V Date: Tue, 28 Apr 2026 23:12:43 +0700 Subject: [PATCH 2/4] Cover Claude permission-mode selection - Assert explicit Claude permission modes suppress cco's default bypass flag - Cover trusted default Auto Mode settings - Verify disabled Auto Mode falls back to the existing bypass default This protects the Auto Mode launch decision without starting real Claude sessions. --- tests/test_startup_preflights.sh | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_startup_preflights.sh b/tests/test_startup_preflights.sh index 545cca5..113ba77 100644 --- a/tests/test_startup_preflights.sh +++ b/tests/test_startup_preflights.sh @@ -214,6 +214,63 @@ else fail "Docker image cleanup skips removal without prompt confirmation" fi +echo "" +echo "Test: Claude permission args honor explicit permission mode" +if ( + source "$FUNCTIONS_ONLY" + claude_args=(--permission-mode auto) + claude_permission_args=(stale) + build_claude_permission_args + [[ ${#claude_permission_args[@]} -eq 0 ]] +); then + pass "explicit Claude permission mode suppresses default bypass flag" +else + fail "explicit Claude permission mode suppresses default bypass flag" +fi + +echo "" +echo "Test: Claude permission args honor trusted default auto mode" +if ( + source "$FUNCTIONS_ONLY" + claude_args=() + claude_permission_args=(stale) + claude_dir="$TEST_ROOT/auto-mode-claude-config" + mkdir -p "$claude_dir" + printf '{"permissions":{"defaultMode":"auto"}}\n' >"$claude_dir/settings.json" + find_claude_config_dir() { + printf '%s\n' "$claude_dir" + } + build_claude_permission_args + [[ ${#claude_permission_args[@]} -eq 0 ]] +); then + pass "trusted default auto mode suppresses default bypass flag" +else + fail "trusted default auto mode suppresses default bypass flag" +fi + +echo "" +echo "Test: Claude permission args keep bypass when auto mode is disabled" +if ( + source "$FUNCTIONS_ONLY" + claude_args=() + claude_permission_args=() + default_settings="$TEST_ROOT/auto-default-settings.json" + disabled_settings="$TEST_ROOT/auto-disabled-settings.json" + printf '{"permissions":{"defaultMode":"auto"}}\n' >"$default_settings" + printf '{"permissions":{"disableAutoMode":"disable"}}\n' >"$disabled_settings" + trusted_claude_settings_paths() { + printf '%s\n' "$disabled_settings" + printf '%s\n' "$default_settings" + } + build_claude_permission_args + [[ ${#claude_permission_args[@]} -eq 1 ]] + [[ "${claude_permission_args[0]}" == "--dangerously-skip-permissions" ]] +); then + pass "disabled auto mode keeps default bypass flag" +else + fail "disabled auto mode keeps default bypass flag" +fi + echo "" echo "Test: OAuth preflight repairs expired credentials before startup" if ( From b15a53329cdbba1305786f100ef3716b53c4fef8 Mon Sep 17 00:00:00 2001 From: Nik V Date: Tue, 28 Apr 2026 23:12:58 +0700 Subject: [PATCH 3/4] Document Claude permission-mode passthrough - Show --permission-mode auto in the Claude pass-through examples - Explain that explicit permission modes prevent cco from adding its bypass default - Note that trusted default Auto Mode settings are preserved when not disabled This makes the Auto Mode behavior discoverable without changing the command surface. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5045f43..bddecc2 100644 --- a/README.md +++ b/README.md @@ -320,11 +320,14 @@ DATABASE_URL=postgres://localhost/mydb cco --resume cco --model claude-3-5-sonnet-20241022 "write tests" cco --no-clipboard "analyze this file" +cco --permission-mode auto "work through this task" # Mix cco and Claude options cco --env DEBUG=1 --resume # `cco` + Claude options ``` +When you pass a Claude permission mode directly, `cco` leaves it alone instead of adding its default bypass flag. If your trusted Claude settings set `permissions.defaultMode` to `auto` and Auto Mode is not disabled, `cco` also lets that default apply. + ### Run Arbitrary Commands (`--command`) You can use `cco` as a generic sandbox wrapper for any CLI, not just Claude. This is helpful when you want a tool to run with full autonomy inside a contained environment. From 15ed3d65fda8f771138b9ba5b1593cd26f276766 Mon Sep 17 00:00:00 2001 From: Nik V Date: Thu, 30 Apr 2026 21:58:30 +0700 Subject: [PATCH 4/4] Handle empty Claude args so Auto Mode checks work on Bash 3.2 - Return early when the Claude argument array is declared but empty - Avoid iterating an empty array under nounset on older Bash versions - Keep explicit permission-mode detection unchanged for populated argument lists This prevents Auto Mode default settings from failing during startup preflight checks on macOS runners. --- cco | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cco b/cco index c928067..e1e5e00 100755 --- a/cco +++ b/cco @@ -275,6 +275,10 @@ claude_args_include_permission_mode() { return 1 fi + if ((${#claude_args[@]} == 0)); then + return 1 + fi + for arg in "${claude_args[@]}"; do case "$arg" in --permission-mode | --permission-mode=* | --dangerously-skip-permissions | --allow-dangerously-skip-permissions)