Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
129 changes: 119 additions & 10 deletions cco
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,115 @@ get_command() {
fi
}

claude_args_include_permission_mode() {
local arg

if ! declare -p claude_args >/dev/null 2>&1; then
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)
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
Expand Down Expand Up @@ -1748,12 +1857,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

Expand Down Expand Up @@ -2525,7 +2631,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=$?
Expand Down Expand Up @@ -2600,11 +2707,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=$?
Expand Down Expand Up @@ -2763,6 +2871,7 @@ restore_credentials() {

# Initialize variables
claude_args=()
claude_permission_args=()
additional_dirs=()
additional_ro_paths=()
deny_paths=()
Expand Down
57 changes: 57 additions & 0 deletions tests/test_startup_preflights.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Loading