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
29 changes: 8 additions & 21 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
# Clayde

**Name:** Clayde
**Email:** clayde@vtettenborn.net
**GitHub:** @ClaydeCode
Clayde is a persistent autonomous AI software agent running in a Docker container. My purpose is to help with software development by working on GitHub issues assigned to me. When assigned an issue, I analyze the relevant codebase, implement a solution, open a pull request, and post a comment on the issue summarizing what I did.

Clayde is a persistent autonomous AI software agent running on a dedicated VM at `/home/ubuntu/clayde`. My purpose is to help with software development by working on GitHub issues assigned to me. When assigned an issue, I analyze the relevant codebase, implement a solution, open a pull request, and post a comment on the issue summarizing what I did.

The `gh` CLI is authenticated as @ClaydeCode and git is configured with my name and email.
The `gh` CLI is authenticated as the configured bot GitHub account and git is configured with the identity from `CLAYDE_GIT_NAME` and `CLAYDE_GIT_EMAIL`.

---

Expand All @@ -27,7 +23,7 @@ The `gh` CLI is authenticated as @ClaydeCode and git is configured with my name
- **Container layout:** Application code at `/opt/clayde`, data at `/data` (single volume mount from host `./data`)
- **Claude:** Dual backend — Anthropic Python SDK (`api`) or Claude Code CLI (`cli`), selected by `CLAYDE_CLAUDE_BACKEND`
- **Git credential helper:** `gh auth git-credential` (configured globally in the container)
- **Git identity:** `user.name = Clayde`, `user.email = clayde@vtettenborn.net`
- **Git identity:** configured at container startup from `CLAYDE_GIT_NAME` and `CLAYDE_GIT_EMAIL` env vars

---

Expand All @@ -39,7 +35,6 @@ pyproject.toml # hatchling build; console scripts: clayde, clayde-once
CLAUDE.md # this file — identity + project context
Dockerfile # Python 3.13-slim image with git, gh, uv
docker-compose.yml # container deployment config
gh-issue.md # slash-command prompt for interactive issue work
uv.lock
src/clayde/
__init__.py
Expand Down Expand Up @@ -99,10 +94,12 @@ Plain `KEY=VALUE` file (no shell quoting). All keys use `CLAYDE_` prefix and are

| Key | Purpose |
|-----|---------|
| `CLAYDE_GITHUB_TOKEN` | Fine-grained PAT with Issues R/W, Pull Requests R/W, Contents R/W |
| `CLAYDE_GITHUB_USERNAME` | `ClaydeCode` |
| `CLAYDE_GITHUB_TOKEN` | Classic PAT with full `repo` scope |
| `CLAYDE_GITHUB_USERNAME` | The bot account username (e.g. `YourBotName`) |
| `CLAYDE_ENABLED` | Set to `true` to activate; any other value causes immediate exit |
| `CLAYDE_WHITELISTED_USERS` | Comma-separated list of trusted GitHub usernames (e.g. `max-tet,ClaydeCode`) |
| `CLAYDE_WHITELISTED_USERS` | Comma-separated list of trusted GitHub usernames |
| `CLAYDE_GIT_NAME` | Git commit author name (defaults to `CLAYDE_GITHUB_USERNAME` if not set) |
| `CLAYDE_GIT_EMAIL` | Git commit author email (required) |
| `CLAYDE_CLAUDE_API_KEY` | Anthropic API key for Claude SDK calls (required when backend=`api`) |
| `CLAYDE_CLAUDE_MODEL` | Model to use (default: `claude-opus-4-6`) |
| `CLAYDE_CLAUDE_BACKEND` | `api` (default) or `cli` — selects Anthropic SDK or Claude Code CLI |
Expand Down Expand Up @@ -290,16 +287,6 @@ Logger names: `clayde.orchestrator`, `clayde.tasks.plan`, `clayde.tasks.implemen

---

## Interactive Issue Work (`gh-issue.md`)

The file `gh-issue.md` is a Claude Code slash-command prompt (`/gh-issue <number>`) for working on issues interactively (outside cron). It runs as a multi-step subagent workflow: Plan → clarify → implement → self-review → address review → return PR URL. Sends push notifications via `apprise ntfy://7yuau0vyes`.

Allowed tools for interactive work: `Bash(gh:*)`, `Bash(git:*)`, `Bash(just:*)`, `Bash(python:*)`, `Bash(pytest:*)`, `Bash(npm:*)`, `Bash(uv:*)`, `Bash(apprise:*)`, `Read`, `Write`, `Edit`, `Glob`, `Grep`

Branch naming for interactive work: `issue/{number}-short-desc`

---

## Testing

Run the test suite after any feature development or bug fix:
Expand Down
12 changes: 7 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ COPY src/ src/
COPY CLAUDE.md ./
RUN uv sync --frozen --no-dev

# Copy entrypoint script
COPY entrypoint.sh /opt/clayde/entrypoint.sh
RUN chmod +x /opt/clayde/entrypoint.sh

# Create data directories and set ownership
RUN mkdir -p /data/repos /data/logs && chown -R clayde:clayde /data

# Switch to non-root user and configure git
# Switch to non-root user and configure git credential helper
USER clayde
RUN mkdir -p /home/clayde/.claude && \
git config --global credential.helper '!gh auth git-credential' && \
git config --global user.name "Clayde" && \
git config --global user.email "clayde@vtettenborn.net"
git config --global credential.helper '!gh auth git-credential'

ENTRYPOINT ["/opt/clayde/.venv/bin/clayde"]
ENTRYPOINT ["/opt/clayde/entrypoint.sh"]
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,56 @@ Clayde will start its loop, checking for assigned issues every 5 minutes (config

| Key | Purpose |
|---|---|
| `CLAYDE_GITHUB_TOKEN` | Fine-grained PAT (Issues R/W, PRs R/W, Contents R/W) |
| `CLAYDE_GITHUB_USERNAME` | `ClaydeCode` |
| `CLAYDE_GITHUB_TOKEN` | Classic PAT with full `repo` scope |
| `CLAYDE_GITHUB_USERNAME` | The bot account username |
| `CLAYDE_GIT_NAME` | Git commit author name (defaults to `CLAYDE_GITHUB_USERNAME` if not set) |
| `CLAYDE_GIT_EMAIL` | Git commit author email (required) |
| `CLAYDE_ENABLED` | Set to `true` to activate |
| `CLAYDE_WHITELISTED_USERS` | Comma-separated trusted GitHub usernames |
| `CLAYDE_CLAUDE_BACKEND` | `api` (default) or `cli` |
| `CLAYDE_CLAUDE_API_KEY` | Anthropic API key (required when backend=`api`) |
| `CLAYDE_CLAUDE_MODEL` | Model to use (default: `claude-opus-4-6`) |

---

## Deploying Your Own Instance

Clayde is designed to be deployed by anyone. To run your own instance:

### 1. Create a dedicated bot GitHub account

Create a GitHub account for your bot (e.g. `my-bot`). This is the account that will be assigned issues and open pull requests.

### 2. Create a GitHub Personal Access Token for the bot

From the bot account, create a classic personal access token with the full **`repo`** scope.

### 3. Configure the instance

```bash
mkdir -p data/logs data/repos
cp config.env.template data/config.env
```

Edit `data/config.env`:

```
CLAYDE_GITHUB_TOKEN=github_pat_...
CLAYDE_GITHUB_USERNAME=my-bot
CLAYDE_GIT_EMAIL=my-bot@example.com
CLAYDE_ENABLED=true
CLAYDE_WHITELISTED_USERS=your-username,my-bot
```

### 4. Choose a Claude backend and start

Follow the backend instructions in the [Setup](#setup) section above, then run:

```bash
docker compose up -d
```

### 5. Assign issues to your bot

In any repository the bot has access to, assign issues to the bot account. Clayde will pick them up automatically on the next loop cycle.

18 changes: 14 additions & 4 deletions config.env.template
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# Clayde configuration — copy to config.env and fill in values
# Clayde configuration — copy to data/config.env and fill in your values

# GitHub identity: the bot account Clayde runs as
CLAYDE_GITHUB_USERNAME=your-bot-username
CLAYDE_GITHUB_TOKEN=
CLAYDE_GITHUB_USERNAME=ClaydeCode

# Git identity for commits (name defaults to CLAYDE_GITHUB_USERNAME if not set)
CLAYDE_GIT_NAME=
CLAYDE_GIT_EMAIL=your-bot@example.com

# Set to true to activate Clayde; any other value causes immediate exit
CLAYDE_ENABLED=false
CLAYDE_WHITELISTED_USERS_RAW=max-tet,ClaydeCode

# Comma-separated list of trusted GitHub usernames allowed to approve plans
CLAYDE_WHITELISTED_USERS=your-username,your-bot-username

# Claude backend: "api" (Anthropic SDK, requires CLAYDE_CLAUDE_API_KEY)
# "cli" (Claude Code CLI, requires OAuth credentials mounted)
# "cli" (Claude Code CLI, requires OAuth credentials mounted)
CLAYDE_CLAUDE_BACKEND=api
CLAYDE_CLAUDE_API_KEY=
22 changes: 22 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh
# Configure git identity from environment variables at container startup.
# CLAYDE_GIT_NAME defaults to CLAYDE_GITHUB_USERNAME if not set.
# CLAYDE_GIT_EMAIL is required.

GIT_NAME="${CLAYDE_GIT_NAME:-$CLAYDE_GITHUB_USERNAME}"
GIT_EMAIL="${CLAYDE_GIT_EMAIL}"

if [ -z "$GIT_NAME" ]; then
echo "ERROR: CLAYDE_GIT_NAME (or CLAYDE_GITHUB_USERNAME) must be set" >&2
exit 1
fi

if [ -z "$GIT_EMAIL" ]; then
echo "ERROR: CLAYDE_GIT_EMAIL must be set" >&2
exit 1
fi

git config --global user.name "$GIT_NAME"
git config --global user.email "$GIT_EMAIL"

exec /opt/clayde/.venv/bin/clayde "$@"
10 changes: 8 additions & 2 deletions src/clayde/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ class Settings(BaseSettings):
)

github_token: str = ""
github_username: str = "ClaydeCode"
github_username: str = ""
enabled: bool = False
whitelisted_users: str = "max-tet,ClaydeCode"
whitelisted_users: str = ""
claude_api_key: str = ""
claude_model: str = "claude-opus-4-6"
claude_backend: str = "api" # "api" or "cli"
git_name: str = ""
git_email: str = ""

@property
def effective_git_name(self) -> str:
return self.git_name or self.github_username

# Claude invocation tuning
claude_tool_loop_timeout_s: int = 1800
Expand Down
17 changes: 15 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,24 @@ def test_defaults(self, monkeypatch):
monkeypatch.delenv("CLAYDE_GITHUB_TOKEN", raising=False)
monkeypatch.delenv("CLAYDE_ENABLED", raising=False)
monkeypatch.delenv("CLAYDE_WHITELISTED_USERS", raising=False)
monkeypatch.delenv("CLAYDE_GITHUB_USERNAME", raising=False)
s = Settings(_env_file=None)
assert s.github_token == ""
assert s.enabled is False
assert s.github_username == "ClaydeCode"
assert s.whitelisted_users_list == ["max-tet", "ClaydeCode"]
assert s.github_username == ""
assert s.whitelisted_users_list == []

def test_effective_git_name_falls_back_to_username(self, monkeypatch):
monkeypatch.setenv("CLAYDE_GITHUB_USERNAME", "my-bot")
monkeypatch.delenv("CLAYDE_GIT_NAME", raising=False)
s = Settings(_env_file=None)
assert s.effective_git_name == "my-bot"

def test_effective_git_name_uses_explicit_value(self, monkeypatch):
monkeypatch.setenv("CLAYDE_GITHUB_USERNAME", "my-bot")
monkeypatch.setenv("CLAYDE_GIT_NAME", "My Bot")
s = Settings(_env_file=None)
assert s.effective_git_name == "My Bot"

def test_loads_from_env_file(self, tmp_path, monkeypatch):
env_file = tmp_path / "config.env"
Expand Down
Loading