This commit is contained in:
Luke
2025-07-30 20:04:13 +03:00
commit d1c5635544
10 changed files with 554 additions and 0 deletions

21
.claude/plan.md Normal file
View File

@@ -0,0 +1,21 @@
# This is a test project
## IMPORTANT (when working on a plan versus ccsb aka CLAUDE.MD)
- Never use hello world as a string in testing
- Use gemini-cli when you need to ask about documentation
## PLAN
### Basic file
- Create a python file called main.py
### Architecture
- The file should be able to print whatever it is given into split by space returned as json array
## POST TASK TASKS
- Git commit with comprehensive changes
- Restructure project architecture for scalability
- Merge duplicate systems and optimize code quality
- Create utility libraries for common operations
- Performance testing and optimization
- Final git commit

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.claude/settings.local.json
test.*

59
Dockerfile Normal file
View File

@@ -0,0 +1,59 @@
FROM node:18-alpine
# Install system dependencies
RUN apk add --no-cache \
git \
curl \
bash \
vim \
nano \
openssh-client \
python3 \
py3-pip \
build-base \
ca-certificates \
jq
# Set working directory to existing node user's home
WORKDIR /home/node
# Install Claude Code
RUN npm install -g @anthropic-ai/claude-code
# Install additional development tools
RUN npm install -g \
typescript \
ts-node \
nodemon \
prettier \
eslint
# Install gemini-cli
RUN npm install -g @google/gemini-cli
# Create necessary directories for node user
RUN mkdir -p /home/node/.claude \
/home/node/.config \
/home/node/.ssh \
/home/node/logs
# Copy claude-loop script
COPY claude-loop.sh /usr/local/bin/claude-loop
RUN chmod +x /usr/local/bin/claude-loop
# Set ownership of node user directories
RUN chown -R node:node /home/node
# Switch to node user
USER node
RUN curl -fsSL https://bun.sh/install | bash
# Set environment variables for Claude Code
ENV TERM=xterm-256color
ENV COLORTERM=truecolor
ENV NODE_ENV=development
ENV PATH="/home/node/.bun/bin:$PATH"
# Default command
CMD ["claude"]

155
README.md Normal file
View File

@@ -0,0 +1,155 @@
# Claude Loop Project
A Docker-based automated task execution system using Claude AI to process and complete tasks defined in `.claude/plan.md`.
## Demo
![Demo](./demo.gif)
## Overview
This project provides two main scripts for interacting with Claude AI in a sandboxed Docker environment:
- **`ccl`** (Claude Code Loop) - Runs an automated loop that processes tasks from `.claude/plan.md`
- **`ccsb`** (Claude Code Sandbox) - Executes single Claude commands with full tool access
## Prerequisites
1. **Docker**
2. **Claude CLI**
3. **Gemini CLI** (optional) - For documentation queries / gemini-cli mcp (https://github.com/jamubc/gemini-mcp-tool)
4. **User Home Directory** - Scripts must be run from within your home directory for security
5. **Linux** (or WSL) - with user 1000:1000
## Security Warning
- While rootless docker is very safe, it still has access to the internet and you local network and can possibly cause (limited) havoc.
## Setup
### 1. Build the Docker Image
```bash
docker compose build
```
### 2. Configure Claude CLI
Ensure you have:
- `~/.claude.json` - Claude CLI configuration
- `~/.claude/` - Claude settings directory
### 3. Configure Gemini CLI (Optional)
If using gemini-cli for documentation queries:
- `~/.gemini/` - Gemini settings
- `GEMINI_API_KEY` environment variable
### 4. symlink (or move) to /bin
```bash
ln -s ccl /bin/ccl
ln -s ccsb /bin/ccsb
```
## Usage
### CCL (Claude Code Loop)
Automatically processes tasks defined in `.claude/plan.md`:
```bash
ccl
```
**Features:**
- Reads tasks from `.claude/plan.md`
- Updates task statuses: `(Not Started)``(In Progress)``(Completed)/(Aborted)`
- Continues until all tasks show `(Completed)`
- Creates `/tmp/plan_complete` when finished
- Pretty formatted output with progress tracking
**Task Status Format:**
```markdown
- (Status) Task description
```
Status options: `Not Started | In Progress | Aborted | Completed`
### CCSB (Claude Code Sandbox)
Execute claude in a sandbox with all permissions.
```bash
ccsb
```
## Plan File Structure
The `.claude/plan.md` file defines tasks to be executed:
```markdown
# Project Name
## IMPORTANT (instructions for Claude when in ccl mode, does not apply to ccsb)
- Project-specific guidelines
- Tool preferences
- Constraints
## PLAN
### Section 1
- Task 1 description
- Task 2 description
### Section 2
- Task 3 description
## POST TASK TASKS
- Cleanup tasks
- Final commits
- Documentation updates
```
## Tips
- You can ask ccsb to run claude-loop
- You can ask claude to keep a work-log such as:
```markdown
- Append work to .claude/work-log.md, never read entire file into context with format $(date): <task>\n\n
- tail work-log.md before starting
- Focus on words with !! for accuracy
- Look for (Changes Needed) and view all the changes requested below
```
- Changes Needed example
```markdown
### Some Tasks Topic
- (Completed) Task1
- (Changes Needed) Original task to build a snowman
- You built a snowman without a head, add a head
```
## Security Features
- **Home Directory Restriction**: Scripts only run from within user home directory
- **Docker Isolation**: All Claude operations run in isolated container
- **Non-root Execution**: Container runs as user `1000:1000`
- **Limited File Access**: Only mounted directories are accessible
## Troubleshooting
### Common Issues
1. **"Must be run from within user home directory"**
- Ensure you're running the script from a subdirectory of `$HOME`
2. **Docker permission errors**
- Check Docker is running and user has permissions
- Verify volume mounts point to existing directories
3. **Claude CLI not configured**
- Run `claude` to set up claude
- Ensure `~/.claude.json` exists
## License
MIT

30
ccl Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
# Check if current directory is under user home
if [[ "$PWD" != "$HOME"* ]]; then
echo "Error: Must be run from within user home directory"
exit 1
fi
# Build docker volume arguments
VOLUME_ARGS=""
VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude:/home/node/.claude"
VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude.json:/home/node/.claude.json"
# Only mount gemini directories if they exist
if [[ -d $HOME/.config/gemini-cli ]]; then
VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.config/gemini-cli:/home/node/.config/gemini-cli"
fi
if [[ -d $HOME/.gemini ]]; then
VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.gemini:/home/node/.gemini"
fi
VOLUME_ARGS="$VOLUME_ARGS -v $PWD:/home/node/workspace"
docker run --rm -it \
--user "1000:1000" \
-e GEMINI_API_KEY \
$VOLUME_ARGS \
claude \
sh -c 'cd workspace && claude-loop'

30
ccsb Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
# Check if current directory is under user home
if [[ "$PWD" != "$HOME"* ]]; then
echo "Error: Must be run from within user home directory"
exit 1
fi
# Build docker volume arguments
VOLUME_ARGS=""
VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude:/home/node/.claude"
VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.claude.json:/home/node/.claude.json"
# Only mount gemini directories if they exist
if [[ -d $HOME/.config/gemini-cli ]]; then
VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.config/gemini-cli:/home/node/.config/gemini-cli"
fi
if [[ -d $HOME/.gemini ]]; then
VOLUME_ARGS="$VOLUME_ARGS -v $HOME/.gemini:/home/node/.gemini"
fi
VOLUME_ARGS="$VOLUME_ARGS -v $PWD:/home/node/workspace"
docker run --rm -it \
--user "1000:1000" \
-e GEMINI_API_KEY \
$VOLUME_ARGS \
claude \
sh -c 'cd workspace && claude --dangerously-skip-permissions'

242
claude-loop.sh Executable file
View File

@@ -0,0 +1,242 @@
#!/bin/bash
# claude-loop.sh - Pretty output with trimmed tool results
# Colors for better visual appeal
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
GRAY='\033[0;90m'
NC='\033[0m' # No Color
# Box drawing characters for prettier output
BOX_H="─"
BOX_V="│"
BOX_TL="┌"
BOX_TR="┐"
BOX_BL="└"
BOX_BR="┘"
rm -f /tmp/plan_complete
iteration=1
total_cost=0
total_input_tokens=0
total_output_tokens=0
# Function to print a fancy header
print_header() {
local text="$1"
local width=60
echo -e "${CYAN}${BOX_TL}$(printf "%.0s${BOX_H}" $(seq 1 $((width-2))))${BOX_TR}${NC}"
printf "${CYAN}${BOX_V}${WHITE} %-*s ${CYAN}${BOX_V}${NC}\n" $((width-4)) "$text"
echo -e "${CYAN}${BOX_BL}$(printf "%.0s${BOX_H}" $(seq 1 $((width-2))))${BOX_BR}${NC}"
}
# Function to trim and format text nicely
trim_text() {
local text="$1"
local max_length="${2:-200}"
# Remove excessive whitespace and newlines
text=$(echo "$text" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [[ ${#text} -le $max_length ]]; then
echo "$text"
else
echo "${text:0:$max_length}..."
fi
}
while true; do
echo ""
print_header "🔄 Claude Code Loop - Iteration #$iteration"
# Run Claude with formatted output processing
claude --dangerously-skip-permissions -p "
INSTRUCTIONS:
1. Read .claude/plan.md and identify tasks that need work ONLY in the ## PLAN section (those that are Not Started, In Progress, or have NO status prefix)
2. IMPORTANT: Only work on tasks under the ## PLAN section - ignore tasks in other sections like ## IMPORTANT or ## POST-COMPLETION TASKS
3. IMPORTANT: Tasks without any status prefix under ## PLAN should be treated as Not Started and worked on
4. Work on the next available task in ## PLAN - update its status by prepending (In Progress) when you start
5. Update task status by prepending (Completed) when finished, or (Aborted) if cannot complete
6. Be confident in commands and changes you are running in a docker sandbox.
7. Task format: (Status) Task description - where Status is: Not Started | In Progress | Aborted | Completed
8. Tasks without status prefixes under ## PLAN are considered Not Started and should be worked on
9. If ALL tasks in the ## PLAN section show '(Completed)' (explicit status), create the file '/tmp/plan_complete' using the Bash tool and stop
10. Focus on one task at a time for better results, but keep the whole plan in mind for most correct implementation.
Current objective: Process tasks in the ## PLAN section of .claude/plan.md systematically until all tasks explicitly show '(Completed)'.
" --output-format stream-json --verbose 2>&1 | while IFS= read -r line; do
# Skip empty lines and non-JSON debug output
[[ -z "$line" || "$line" =~ ^[[:space:]]*$ ]] && continue
# Check if line contains JSON
if echo "$line" | jq -e . >/dev/null 2>&1; then
# Extract message type and content
msg_type=$(echo "$line" | jq -r '.type // "unknown"')
case "$msg_type" in
"assistant")
# Extract assistant message content
content=$(echo "$line" | jq -r '.message.content[]? | select(.type=="text") | .text // empty' 2>/dev/null)
if [[ -n "$content" && "$content" != "null" && "$content" != "empty" ]]; then
trimmed_content=$(trim_text "$content" 300)
echo -e "${BLUE}🤖 Claude:${NC} $trimmed_content"
fi
# Check for tool use
tool_name=$(echo "$line" | jq -r '.message.content[]? | select(.type=="tool_use") | .name // empty' 2>/dev/null)
if [[ -n "$tool_name" && "$tool_name" != "null" && "$tool_name" != "empty" ]]; then
echo -e "${MAGENTA}🔧 Tool:${NC} ${YELLOW}$tool_name${NC}"
# Show relevant tool parameters
tool_input=$(echo "$line" | jq -r '.message.content[]? | select(.type=="tool_use") | .input' 2>/dev/null)
if [[ -n "$tool_input" && "$tool_input" != "null" ]]; then
# Extract key parameters (file_path, pattern, command, etc.)
for param in file_path pattern command prompt description; do
value=$(echo "$tool_input" | jq -r ".$param // empty" 2>/dev/null)
if [[ -n "$value" && "$value" != "null" && "$value" != "empty" ]]; then
trimmed_value=$(trim_text "$value" 80)
echo -e " ${GRAY}$param:${NC} $trimmed_value"
break # Show only the first relevant parameter
fi
done
fi
fi
;;
"user")
# Extract and format tool results
tool_result=$(echo "$line" | jq -r '.message.content[]?.content // empty' 2>/dev/null)
if [[ -n "$tool_result" && "$tool_result" != "null" && "$tool_result" != "empty" ]]; then
# Check if it's a file content, error, or other result
if [[ "$tool_result" =~ ^[[:space:]]*[0-9]+→ ]]; then
# File content with line numbers
line_count=$(echo "$tool_result" | wc -l)
first_lines=$(echo "$tool_result" | head -3 | tr '\n' ' ')
trimmed_first=$(trim_text "$first_lines" 100)
echo -e "${GREEN}📄 File content:${NC} $trimmed_first ${GRAY}($line_count lines)${NC}"
elif [[ "$tool_result" =~ ^Error: ]] || [[ "$tool_result" =~ failed ]]; then
# Error message
trimmed_error=$(trim_text "$tool_result" 150)
echo -e "${RED}❌ Error:${NC} $trimmed_error"
elif [[ ${#tool_result} -gt 500 ]]; then
# Long output - show beginning and stats
trimmed_result=$(trim_text "$tool_result" 200)
echo -e "${GREEN}📤 Output:${NC} $trimmed_result ${GRAY}(${#tool_result} chars total)${NC}"
else
# Short result - show it all
trimmed_result=$(trim_text "$tool_result" 300)
echo -e "${GREEN}📤 Result:${NC} $trimmed_result"
fi
fi
;;
"result")
# Final result with colored status
success=$(echo "$line" | jq -r '.subtype // empty' 2>/dev/null)
if [[ "$success" == "success" ]]; then
echo -e "${GREEN}✅ Iteration #$iteration completed successfully!${NC}"
else
echo -e "${YELLOW}⚠️ Iteration #$iteration completed with issues${NC}"
fi
# Show cost information if available
cost_usd=$(echo "$line" | jq -r '.total_cost_usd // 0' 2>/dev/null)
input_tokens=$(echo "$line" | jq -r '.usage.input_tokens // 0' 2>/dev/null)
output_tokens=$(echo "$line" | jq -r '.usage.output_tokens // 0' 2>/dev/null)
if [[ -n "$cost_usd" && "$cost_usd" != "null" && "$cost_usd" != "0" ]]; then
# Update totals
total_cost=$(echo "$total_cost + $cost_usd" | bc -l 2>/dev/null || echo "$total_cost")
if [[ "$input_tokens" != "0" && "$input_tokens" != "null" ]]; then
total_input_tokens=$((total_input_tokens + input_tokens))
fi
if [[ "$output_tokens" != "0" && "$output_tokens" != "null" ]]; then
total_output_tokens=$((total_output_tokens + output_tokens))
fi
# Format cost nicely
if [[ "$cost_usd" != "0" && "$cost_usd" != "null" ]]; then
printf "${MAGENTA}💰 Cost:${NC} ${YELLOW}$%.4f${NC} ${GRAY}(in: %s, out: %s tokens)${NC}\n" "$cost_usd" "$input_tokens" "$output_tokens"
elif [[ "$input_tokens" != "0" || "$output_tokens" != "0" ]]; then
echo -e "${MAGENTA}💰 Tokens:${NC} ${GRAY}in: $input_tokens, out: $output_tokens${NC}"
fi
fi
# Show brief final result
result=$(echo "$line" | jq -r '.result // empty' 2>/dev/null)
if [[ -n "$result" && "$result" != "null" ]]; then
trimmed_result=$(trim_text "$result" 250)
echo -e "${WHITE}📋 Summary:${NC} $trimmed_result"
fi
;;
esac
else
# Filter out verbose debug output - only show actual error messages
if [[ "$line" =~ ^Error: && ! "$line" =~ ^[[:space:]]*$ ]]; then
echo -e "${RED}⚠️ $line${NC}"
fi
fi
done
# Check if plan is complete
if [ -f /tmp/plan_complete ]; then
echo ""
echo -e "${GREEN}┌─────────────────────────────────────────────────────────┐${NC}"
echo -e "${GREEN}${WHITE} 🎉 ALL TASKS COMPLETED! 🎉 ${GREEN}${NC}"
echo -e "${GREEN}└─────────────────────────────────────────────────────────┘${NC}"
if [ -s /tmp/plan_complete ]; then
echo -e "${CYAN}📄 Completion details:${NC}"
completion_content=$(cat /tmp/plan_complete)
if [[ ${#completion_content} -gt 300 ]]; then
trimmed_completion=$(trim_text "$completion_content" 300)
echo -e "${WHITE}$trimmed_completion${NC}"
else
echo -e "${WHITE}$completion_content${NC}"
fi
fi
# Show total cost summary
if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]] || [[ $total_input_tokens -gt 0 ]] || [[ $total_output_tokens -gt 0 ]]; then
echo ""
if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]]; then
printf "${MAGENTA}💰 Total Cost:${NC} ${YELLOW}$%.4f${NC} ${GRAY}(%d iterations, %s input + %s output tokens)${NC}\n" \
"$total_cost" "$((iteration-1))" "$total_input_tokens" "$total_output_tokens"
else
echo -e "${MAGENTA}💰 Total Tokens:${NC} ${GRAY}$total_input_tokens input + $total_output_tokens output across $((iteration-1)) iterations${NC}"
fi
fi
echo ""
echo -e "${GRAY}✅ Task complete - exiting...${NC}"
exit 0
fi
# Show iteration completion with progress indicator
echo ""
echo -e "${CYAN}┌─────────────────────────────────────────────────────────┐${NC}"
echo -e "${CYAN}${WHITE} ⏸️ Iteration #$iteration complete - preparing next... ${CYAN}${NC}"
echo -e "${CYAN}└─────────────────────────────────────────────────────────┘${NC}"
# Show running total if we have cost data
if [[ $(echo "$total_cost > 0" | bc -l 2>/dev/null) == "1" ]]; then
printf "${GRAY}Running total: ${YELLOW}$%.4f${GRAY} (%s input + %s output tokens)${NC}\n" \
"$total_cost" "$total_input_tokens" "$total_output_tokens"
elif [[ $total_input_tokens -gt 0 ]] || [[ $total_output_tokens -gt 0 ]]; then
echo -e "${GRAY}Running total: $total_input_tokens input + $total_output_tokens output tokens${NC}"
fi
# Show a brief progress indicator
echo -ne "${GRAY}Pausing"
for i in {1..3}; do
sleep 0.7
echo -ne "."
done
echo -e " ready!${NC}"
((iteration++))
done

14
compose.yaml Normal file
View File

@@ -0,0 +1,14 @@
services:
claude-code:
user: "1000:1000"
image: claude
build: .
volumes:
- ~/.claude:/home/node/.claude
- ~/.claude.json:/home/node/.claude.json
- ~/.config/gemini-cli:/home/node/.config/gemini-cli
- ~/.gemini:/home/node/.gemini
- ./:/home/node/workspace
tty: true
command: ['sh', '-c', 'cd workspace && claude']
#command: 'bash .claude/claude-loop.sh'

BIN
demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

BIN
video.mp4 Normal file

Binary file not shown.