Skip to content

Instantly share code, notes, and snippets.

@metcalfc
Last active August 6, 2025 02:55
Show Gist options
  • Save metcalfc/04871f1e1aea5c95e7884738e4f42b1b to your computer and use it in GitHub Desktop.
Save metcalfc/04871f1e1aea5c95e7884738e4f42b1b to your computer and use it in GitHub Desktop.
cnme - Safe containerized wrapper for Continue CLI

cnme - Continue CLI Containerized for ME

Safe containerized wrapper for Continue CLI that prevents dangerous commands from affecting your host system.

The Problem

Continue CLI's runTerminalCommand tool executes shell commands directly on your host system. This can be dangerous - imagine if Claude decides to run rm -rf / or other destructive commands that could damage your machine.

The Solution

cnme (Continue CLI containerized for ME) wraps Continue CLI in a Docker container, providing complete isolation while maintaining full functionality. All dangerous commands are contained within a disposable container that can't harm your host system.

Quick Start

Prerequisites

  • Docker installed and running
  • Continue CLI project directory

Try it out

  1. Make cnme executable:

    chmod +x cnme
  2. Interactive mode (same as cn):

    ./cnme
  3. Headless mode:

    ./cnme -p "Help me debug this function"
  4. Silent mode (clean output):

    ./cnme -p "Fix this bug" --silent
  5. All Continue CLI features work:

    ./cnme login                    # Authentication
    ./cnme serve                    # HTTP server mode
    ./cnme --readonly              # Read-only mode
    ./cnme --format json -p "test" # JSON output

How It Works

Host System                    Container Environment
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ $ cnme -p "fix bug" β”‚ ───── β”‚ Full Continue CLI           β”‚
β”‚                     β”‚       β”‚ + All development tools     β”‚
β”‚ Mounts:             β”‚       β”‚                             β”‚
β”‚ β€’ $PWD β†’ /workspace β”‚       β”‚ πŸ›‘οΈ  Dangerous commands      β”‚
β”‚ β€’ ~/.continue       β”‚       β”‚    isolated in container   β”‚
β”‚   β†’ /home/user/.con β”‚       β”‚    Host system protected   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What Gets Mounted

  • Project directory ($PWD β†’ /workspace) - Read/write for your work
  • Continue config (~/.continue) - Read/write for auth/sessions
  • SSH keys (~/.ssh) - Read-only for git operations
  • Git config (~/.gitconfig) - Read-only for git operations

Safety Features

βœ… Complete isolation - Host system protected from dangerous commands
βœ… Container disposal - Each session uses a fresh, disposable container
βœ… User matching - Files maintain proper ownership via UID/GID matching
βœ… Resource limits - Memory (2GB) and CPU constraints prevent resource exhaustion
βœ… No privilege escalation - Container cannot gain additional privileges
βœ… Controlled networking - Network access only for Continue CLI authentication/API calls

Development Tools Included

The container includes all tools Claude commonly uses:

  • Core: bash, git, curl, wget, jq, sqlite3
  • Text processing: sed, awk, grep, ripgrep, find
  • Development: python3, node/npm, build tools
  • File operations: tree, tar, zip, file utilities

cnme-Specific Options

  • --shell - Drop into container shell for debugging
  • --rebuild-image - Force rebuild of container image
  • --unsafe - Bypass container (run cn directly - not recommended)

Transparent Usage

cnme is designed to be a completely transparent replacement for cn:

# Instead of:          # Use:
cn                      ./cnme
cn -p "fix bug"        ./cnme -p "fix bug"  
cn --readonly          ./cnme --readonly
cn login               ./cnme login

All Continue CLI arguments, options, and functionality work exactly the same.

Getting Help

./cnme --help    # Shows both cnme and Continue CLI help

Example Session

# Start interactive session (safe)
./cnme

# In the session, dangerous commands are contained:
> "Clean up all temporary files in the system"
# Any rm, find, or system commands run in container only!

# Your host system remains completely safe

Architecture Notes

  • Dynamic user matching: Container user matches your host UID/GID at build time
  • Process management: Uses tini for proper signal handling and zombie reaping
  • Build caching: Container image built once per user, reused for performance
  • Cross-platform: Works on Linux and macOS with Docker

Debugging

If something goes wrong:

  1. Check Docker:

    docker ps  # See if containers are running
  2. Rebuild image:

    ./cnme --rebuild-image
  3. Debug shell:

    ./cnme --shell
  4. Bypass for comparison:

    ./cnme --unsafe -p "same command"

Performance Notes

  • First run: Slower (builds container image)
  • Subsequent runs: Fast (reuses built image)
  • File operations: Slightly slower due to Docker volume mounts
  • Network operations: Same performance as native cn

When to Use cnme vs cn

Use cnme when:

  • Working with untrusted code or projects
  • Claude might run system commands you're unsure about
  • You want guaranteed safety during development
  • Working on shared/production systems

Use native cn when:

  • Maximum performance needed
  • Working in already-isolated environments (VMs, etc.)
  • Debugging cnme itself

cnme provides peace of mind - all the power of Continue CLI with none of the risk to your host system.

#!/usr/bin/env bash
# cnme - Continue CLI containerized for ME
# Safe containerized wrapper for Continue CLI that provides complete isolation
# while maintaining full development functionality
# Bash strict mode for better error handling
set -euo pipefail
# IFS (Internal Field Separator) - controls how bash splits strings
# Setting to newline and tab prevents issues with spaces in filenames/paths
IFS=$'\n\t'
# Configuration - using readonly for constants to prevent accidental changes
readonly CONTAINER_IMAGE="cnme:latest"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Generate unique container name to avoid conflicts
readonly CONTAINER_NAME="cnme-$$-$(date +%s)"
# Cleanup function - called on script exit to ensure containers are cleaned up
cleanup() {
local exit_code=$?
if [[ -n "${CONTAINER_NAME:-}" ]]; then
# Kill container if it's still running (suppress errors since it might not exist)
docker kill "$CONTAINER_NAME" 2>/dev/null || true
fi
exit $exit_code
}
# Set cleanup to run on script exit (normal or error)
trap cleanup EXIT
# Colors for output - readonly to prevent accidental modification
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m' # No Color
# Logging functions - all output to stderr to avoid interfering with command output
log() {
# Use printf instead of echo -e for better portability
printf '%b[cnme]%b %s\n' "$BLUE" "$NC" "$1" >&2
}
warn() {
printf '%b[cnme] WARNING:%b %s\n' "$YELLOW" "$NC" "$1" >&2
}
error() {
printf '%b[cnme] ERROR:%b %s\n' "$RED" "$NC" "$1" >&2
exit 1
}
success() {
printf '%b[cnme]%b %s\n' "$GREEN" "$NC" "$1" >&2
}
# Check if we're already inside a container
is_containerized() {
[[ "${CNME_CONTAINERIZED:-}" == "true" ]]
}
# Check if this is headless mode (non-interactive)
is_headless_mode() {
local args=("$@")
for arg in "${args[@]}"; do
case "$arg" in
-p|--print)
return 0
;;
esac
done
# Also check for --silent or --format which indicate headless output formatting
for arg in "${args[@]}"; do
case "$arg" in
--silent|--format)
return 0
;;
esac
done
return 1
}
# Check if Docker is available and running
check_docker() {
# Check if docker command exists
if ! command -v docker >/dev/null 2>&1; then
case "$OSTYPE" in
darwin*)
error "Docker not found. Install with: brew install docker colima && colima start"
;;
linux-gnu*)
error "Docker not found. Install with: apt-get install docker.io (or equivalent)"
;;
*)
error "Docker not found. Please install Docker for your system."
;;
esac
fi
# Check if docker daemon is running - capture both stdout and stderr
if ! docker ps >/dev/null 2>&1; then
# More specific error message based on common failure modes
if docker version --format '{{.Client.Version}}' >/dev/null 2>&1; then
error "Docker daemon not running. Please start Docker."
else
error "Docker not accessible. Check installation and permissions."
fi
fi
}
# Build container image if it doesn't exist
ensure_image() {
if docker image inspect "$CONTAINER_IMAGE" >/dev/null 2>&1; then
log "Using existing container image: $CONTAINER_IMAGE"
return
fi
log "Building container image: $CONTAINER_IMAGE"
log "This may take a few minutes the first time..."
# Check if Dockerfile exists - use SCRIPT_DIR for more robust path handling
local dockerfile_path="$SCRIPT_DIR/Dockerfile"
if [[ ! -f "$dockerfile_path" ]]; then
error "Dockerfile not found at: $dockerfile_path"
fi
# Get current user's UID, GID, and username for matching inside container
local host_uid host_gid host_user
host_uid=$(id -u)
host_gid=$(id -g)
host_user=$(whoami)
log "Building with host user: $host_user ($host_uid:$host_gid)"
# Build the image with host user info as build args
# Use SCRIPT_DIR for build context and add --quiet for cleaner output
if ! docker build \
--build-arg HOST_UID="$host_uid" \
--build-arg HOST_GID="$host_gid" \
--build-arg HOST_USER="$host_user" \
--tag "$CONTAINER_IMAGE" \
--file "$dockerfile_path" \
"$SCRIPT_DIR"; then
error "Failed to build container image. Check Docker daemon and Dockerfile."
fi
success "Container image built successfully"
}
# Get mount configurations with validation
get_mounts() {
local quiet=${1:-false}
local host_user=${2:-$(whoami)}
local mounts=()
# Validate inputs
if [[ -z "$host_user" ]]; then
error "Host user not specified for mount configuration"
fi
# Always mount current directory as workspace - ensure PWD is set
if [[ -z "${PWD:-}" ]]; then
error "Current directory (PWD) not available for mounting"
fi
mounts+=("-v" "$PWD:/workspace")
if [[ "$quiet" != "true" ]]; then
log "Mounting workspace: $PWD (rw)"
fi
# Mount Continue config if it exists (read-write for authentication/sessions)
if [[ -d "$HOME/.continue" ]]; then
mounts+=("-v" "$HOME/.continue:/home/${host_user}/.continue:rw")
if [[ "$quiet" != "true" ]]; then
log "Mounting Continue config: $HOME/.continue (rw)"
fi
else
if [[ "$quiet" != "true" ]]; then
warn "Continue config directory not found: $HOME/.continue"
fi
fi
# Optional: Mount SSH keys for git operations (read-only)
if [[ -d "$HOME/.ssh" ]]; then
mounts+=("-v" "$HOME/.ssh:/home/${host_user}/.ssh:ro")
if [[ "$quiet" != "true" ]]; then
log "Mounting SSH keys: $HOME/.ssh (ro)"
fi
fi
# Optional: Mount git config (read-only)
if [[ -f "$HOME/.gitconfig" ]]; then
mounts+=("-v" "$HOME/.gitconfig:/home/${host_user}/.gitconfig:ro")
if [[ "$quiet" != "true" ]]; then
log "Mounting git config: $HOME/.gitconfig (ro)"
fi
fi
# Output all mounts - one per line for easier parsing
printf "%s\n" "${mounts[@]}"
}
# Run Continue CLI in container
run_containerized() {
check_docker
if [[ "$silent_mode" != "true" ]]; then
ensure_image
else
# Silent mode - ensure image exists but don't show output
ensure_image >/dev/null 2>&1
fi
# Only show verbose output if not in headless/silent mode
if ! is_headless_mode "$@" && [[ "$silent_mode" != "true" ]]; then
log "Starting Continue CLI in secure container..."
log "Project directory: $PWD β†’ /workspace"
fi
# Prepare docker run arguments - using array for safer argument handling
local docker_args=(
"run"
"--rm" # Remove container when it exits
"--interactive" # Keep stdin open
"--name" "$CONTAINER_NAME"
)
# Add TTY if we have one (check if stdin is a terminal)
if [[ -t 0 ]]; then
docker_args+=("--tty")
fi
# Get host user info for dynamic configuration
local host_user
host_user=$(whoami)
# Add all mounts (quiet mode for headless or silent)
local mounts
local quiet_mounts="false"
if is_headless_mode "$@" || [[ "$silent_mode" == "true" ]]; then
quiet_mounts="true"
fi
readarray -t mounts < <(get_mounts "$quiet_mounts" "$host_user")
docker_args+=("${mounts[@]}")
# Security settings
docker_args+=(
"--user" "$host_user" # Host-matching user
"--network" "bridge" # Network access needed for Continue auth/API
"--memory" "2g" # Memory limit
"--cpus" "2.0" # CPU limit
"--security-opt" "no-new-privileges" # Prevent privilege escalation
)
# Environment variables
docker_args+=(
"-e" "CNME_CONTAINERIZED=true" # Mark as containerized
"-e" "HOME=/home/$host_user" # Set proper home directory
"-e" "USER=$host_user" # Set user
)
# Add the image and command
docker_args+=("$CONTAINER_IMAGE" "cn")
# Add all original arguments
docker_args+=("$@")
# Execute the container - exec replaces current process
# Cleanup is handled by the global trap set at script start
exec docker "${docker_args[@]}"
}
# Show usage information
show_usage() {
echo "cnme - Continue CLI containerized for ME"
echo
echo "TRANSPARENT CONTAINERIZED WRAPPER:"
echo " All Continue CLI commands and options work exactly the same."
echo " cnme adds complete safety through Docker container isolation."
echo
echo "EXAMPLES:"
echo " cnme # Interactive mode"
echo " cnme -p \"Fix this bug\" # Headless mode"
echo " cnme --format json -p \"Test\" # JSON output"
echo " cnme --readonly # Read-only mode"
echo " cnme login # Authentication"
echo " cnme serve # HTTP server mode"
echo
echo "CNME-SPECIFIC OPTIONS:"
echo " --unsafe Bypass container (run cn directly - not recommended)"
echo " --rebuild-image Force rebuild of container image"
echo " --shell Drop into container shell for debugging"
echo
echo "SECURITY FEATURES:"
echo " βœ… Complete isolation from host system"
echo " βœ… All dangerous commands contained in disposable container"
echo " βœ… Host user matching for proper file permissions"
echo " βœ… Memory/CPU limits and controlled network access"
echo " βœ… No privilege escalation allowed"
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}
# Main execution logic
main() {
# If already containerized, this shouldn't happen with our design
# but just in case, pass through to real cn
if is_containerized; then
exec cn "$@"
fi
# Parse arguments once to detect silent mode
local silent_mode=false
for arg in "$@"; do
case "$arg" in
--silent)
silent_mode=true
break
;;
esac
done
# Handle cnme-specific flags first, before passing to Continue CLI
local cnme_args=()
local cn_args=()
local skip_next=false
for arg in "$@"; do
if [[ "$skip_next" == "true" ]]; then
skip_next=false
continue
fi
case "$arg" in
--help|-h)
# Show both cnme and cn help
show_usage
echo
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Continue CLI (cn) Help:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
run_containerized --help
exit 0
;;
--unsafe)
warn "Running Continue CLI in UNSAFE mode (no container protection)"
# Remove this argument and run cn directly with remaining args
shift
# Try to find cn binary
if command -v cn >/dev/null 2>&1; then
exec cn "$@"
else
error "Continue CLI (cn) not found. Install with: npm install -g @continuedev/cli"
fi
;;
--rebuild-image)
log "Removing existing container image..."
docker rmi "$CONTAINER_IMAGE" 2>/dev/null || true
# Remove this arg and continue with remaining args
cnme_args+=("$arg")
;;
--shell)
check_docker
ensure_image
log "Opening debug shell in container..."
local host_user mounts
host_user=$(whoami)
readarray -t mounts < <(get_mounts "false" "$host_user")
exec docker run -it --rm \
"${mounts[@]}" \
--user "$host_user" \
--network bridge \
--memory 2g \
--cpus 2.0 \
--security-opt no-new-privileges \
-e "CNME_CONTAINERIZED=true" \
-e "HOME=/home/$host_user" \
-e "USER=$host_user" \
"$CONTAINER_IMAGE" bash
;;
*)
# All other arguments go to Continue CLI
cn_args+=("$arg")
;;
esac
done
# Remove cnme-specific args from the original argument list
set -- "${cn_args[@]}"
# Run in containerized mode with remaining args
run_containerized "$@"
}
# Execute main function with all arguments
main "$@"
# Continue CLI Containerized Environment (cnme)
# Comprehensive development environment with all tools Claude AI might use
FROM node:20-slim
# Set environment variables
ENV DEBIAN_FRONTEND=noninteractive
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
# Install comprehensive toolset for Claude AI workflows
RUN apt-get update && apt-get install -y \
# Core system utilities
bash \
coreutils \
util-linux \
procps \
psmisc \
# Text processing tools
sed \
gawk \
grep \
diffutils \
# File system operations
findutils \
tree \
less \
file \
# Network tools
curl \
wget \
netcat-openbsd \
iputils-ping \
# Development essentials
git \
build-essential \
python3 \
python3-pip \
# Data processing
jq \
sqlite3 \
# Archive tools
tar \
gzip \
zip \
unzip \
# Search and text tools
ripgrep \
# Editors
vim \
nano \
# System utilities
ca-certificates \
gnupg \
lsb-release \
# Additional utilities Claude commonly uses
bc \
dc \
rsync \
patch \
&& rm -rf /var/lib/apt/lists/*
# Install tini for proper process management (signal handling, zombie reaping)
RUN curl -fsSL https://github.com/krallin/tini/releases/download/v0.19.0/tini-amd64 -o /usr/local/bin/tini && \
chmod +x /usr/local/bin/tini
# Install Continue CLI globally
RUN npm install -g @continuedev/cli
# Create user with host-matching UID/GID and username (passed as build args)
ARG HOST_UID=1000
ARG HOST_GID=1000
ARG HOST_USER=continue
RUN userdel -r node 2>/dev/null || true && \
groupdel node 2>/dev/null || true && \
groupadd -g ${HOST_GID} ${HOST_USER} && \
useradd -m -u ${HOST_UID} -g ${HOST_GID} -s /bin/bash ${HOST_USER}
# Setup workspace and config directories with proper ownership
ARG HOST_USER=continue
RUN mkdir -p /workspace /home/${HOST_USER}/.continue && \
chown -R ${HOST_USER}:${HOST_USER} /workspace /home/${HOST_USER}
# Create helpful aliases and environment for development
RUN echo 'alias ll="ls -la"' >> /home/${HOST_USER}/.bashrc && \
echo 'alias grep="grep --color=auto"' >> /home/${HOST_USER}/.bashrc && \
echo 'export PATH="/home/${HOST_USER}/.local/bin:$PATH"' >> /home/${HOST_USER}/.bashrc
# No startup script needed - UID/GID matching handles file ownership
# Switch to host-matching user
USER ${HOST_USER}
WORKDIR /workspace
# Verify critical tools are available and working
RUN cn --version && \
rg --version && \
git --version && \
jq --version && \
python3 --version && \
curl --version
# Use tini as init system for proper signal handling
ENTRYPOINT ["/usr/local/bin/tini", "--"]
# Set default command to Continue CLI
CMD ["cn"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment