Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
4f276df
feat: switch Claude Code to native installer (self-updating)
itscooleric Mar 10, 2026
47e6183
feat: structured session logging with secret scrubbing and retention …
itscooleric Mar 12, 2026
9d9d03c
fix: use bash instead of sh for Claude native installer
itscooleric Mar 12, 2026
ce9b899
UI improvements: tmux polish + ttyd auto-reconnect
itscooleric Mar 12, 2026
f3cf793
feat: push notifications via ntfy when agent needs attention
itscooleric Mar 12, 2026
e98d7fa
fix: use --client-option for ttyd reconnect (not server flag)
itscooleric Mar 12, 2026
f09a419
docs: add Caddy basicauth label guidance to docker-compose
itscooleric Mar 12, 2026
9cb47fd
fix: scrub ttyd credentials from docker logs
itscooleric Mar 12, 2026
fa3104d
fix: also scrub base64-encoded credential from ttyd logs
itscooleric Mar 13, 2026
3b46683
fix: restore clean exec for ttyd (sed pipe broke PID 1)
itscooleric Mar 13, 2026
aa94b09
feat: install LAN CA cert at runtime via CLIDE_CA_URL
itscooleric Mar 13, 2026
bcd18ed
add debug-notify.sh for testing notification pipeline
itscooleric Mar 13, 2026
8834e6b
refactor: simplify notify.sh to event-based (drop transcript parsing)
itscooleric Mar 13, 2026
65bef69
remove emojis from ntfy notification titles
itscooleric Mar 13, 2026
7f8b233
skip session logging for entrypoint pre-seed (command=true)
itscooleric Mar 13, 2026
df00037
feat: auto-wrap agent CLIs through session-logger via bashrc
itscooleric Mar 14, 2026
c7c1f30
docs: update README and schema for v4 features
itscooleric Mar 14, 2026
1056c51
fix: address shellcheck and markdownlint CI failures
itscooleric Mar 14, 2026
06d5298
fix: correct shellcheck directive syntax (no trailing comments)
itscooleric Mar 14, 2026
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
8 changes: 8 additions & 0 deletions .bashrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# shellcheck shell=bash
# Wrap agent CLIs through session-logger for structured logging + notifications.
# Disable with CLIDE_LOG_DISABLED=1 in .env.
if command -v session-logger.sh >/dev/null 2>&1 && [[ "${CLIDE_LOG_DISABLED:-}" != "1" ]]; then
claude() { session-logger.sh claude "$@"; }
codex() { session-logger.sh codex "$@"; }
copilot() { session-logger.sh copilot "$@"; }
fi
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
.env
docker-compose.override.yml

# Session logs (generated at runtime inside workspace)
.clide/logs/
37 changes: 30 additions & 7 deletions .tmux.conf
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
# Mouse support (click to focus pane, scroll, resize)
# Toggle with F12 for mobile-friendly usage (touch browsers + tmux mouse = pain)
set -g mouse on
bind -T root F12 \
set -g mouse \; \
display "mouse: #{?mouse,on,off}"

# Increase scrollback
set -g history-limit 10000
# Increase scrollback — generous buffer so long agent output isn't lost
set -g history-limit 50000

# 256-colour support
set -g default-terminal "screen-256color"

# Zero escape delay — no lag on Escape key (critical for vim/editor use)
set -g escape-time 0

# Pass focus events to programs (better editor integration)
set -g focus-events on

# Enable clipboard integration via OSC 52 (ttyd → browser clipboard)
set -g set-clipboard on

# Split panes with | and - (intuitive, and keeps current path)
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"

# Reload config with r
bind r source-file /root/.tmux.conf \; display "tmux config reloaded"
bind r source-file ~/.tmux.conf \; display "tmux config reloaded"

# Status bar
set -g status-bg black
set -g status-fg white
set -g status-left "[clide] "
set -g status-right "%H:%M"
set -g status-bg "#1a1a2e"
set -g status-fg "#a0a0c0"
set -g status-left "#[fg=#e0e0ff,bold][clide] "
set -g status-left-length 20
set -g status-right "#[fg=#606080]#{pane_current_command} #[fg=#a0a0c0]%H:%M"
set -g status-right-length 40

# Active pane border
set -g pane-active-border-style "fg=#7070a0"
set -g pane-border-style "fg=#303050"

# Window status
set -g window-status-current-format "#[fg=#e0e0ff,bold] #W "
set -g window-status-format "#[fg=#606080] #W "
17 changes: 11 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ RUN ARCH="$(uname -m)" \
-o /usr/local/bin/ttyd \
&& chmod +x /usr/local/bin/ttyd

# Install Claude Code CLI (pinned — bump ARG to upgrade)
ARG CLAUDE_CODE_VERSION=2.1.71
RUN npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"

# Install Codex CLI (pinned — bump ARG to upgrade)
# hadolint ignore=DL3059
ARG CODEX_VERSION=0.112.0
Expand Down Expand Up @@ -71,7 +67,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
pytest==9.0.2 \
ruff==0.15.5

ENV PATH="/opt/pyenv/bin:${PATH}"
ENV PATH="/home/clide/.local/bin:/opt/pyenv/bin:${PATH}"

# Create unprivileged user and set up workspace
# UID/GID default to 1000 (standard first non-root user on Linux/macOS).
Expand All @@ -89,17 +85,26 @@ RUN groupadd -g "${CLIDE_GID}" clide 2>/dev/null || groupmod -n clide "$(getent
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY claude-entrypoint.sh /usr/local/bin/claude-entrypoint.sh
COPY firewall.sh /usr/local/bin/firewall.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/claude-entrypoint.sh /usr/local/bin/firewall.sh
COPY scripts/session-logger.sh /usr/local/bin/session-logger.sh
COPY scripts/notify.sh /usr/local/bin/notify.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/claude-entrypoint.sh /usr/local/bin/firewall.sh /usr/local/bin/session-logger.sh /usr/local/bin/notify.sh

# Default CLAUDE.md template — seeded into /workspace on first run if none exists
COPY CLAUDE.md.template /usr/local/share/clide/CLAUDE.md.template

# tmux config — mouse support, sane splits, 256-colour
COPY --chown=clide:clide .tmux.conf /home/clide/.tmux.conf

# Shell config — wraps agent CLIs through session-logger automatically
COPY --chown=clide:clide .bashrc /home/clide/.bashrc

# Switch to unprivileged user for user-scoped installs
USER clide

# Install Claude Code CLI via native installer (self-updating, no npm dependency).
# Installs to ~/.local/bin/claude — auto-updates at runtime without sudo.
RUN curl -fsSL https://claude.ai/install.sh | bash

# Trust all directories for git operations.
# Clide is a single-user dev sandbox — volume-mounted repos from the host
# are often owned by a different UID (host user vs clide:1000), which causes
Expand Down
52 changes: 50 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
██ ██ ██ ██ ██ ██
██████ ███████ ██ ██████ ███████

sandboxed agentic terminal v3
sandboxed agentic terminal v4
──────────────────────────────────────

your project ──bind mount──► /workspace
Expand Down Expand Up @@ -113,7 +113,7 @@ codex auth login --auth device

### tmux — multi-pane workflows

`tmux` is installed in the container and enabled by default in the **web terminal**. Every browser tab attaches to the same named session (`main`), so refreshing the page re-attaches rather than spawning a fresh shell.
`tmux` is installed in the container and enabled by default in the **web terminal**. Every browser tab attaches to the same named session (`main`), so refreshing the page re-attaches rather than spawning a fresh shell. The web terminal auto-reconnects after network drops (3s default, configurable via `TTYD_RECONNECT`).

For `make shell` / `./clide shell`, tmux is **opt-in** to avoid breaking existing workflows:
```env
Expand All @@ -130,6 +130,7 @@ CLIDE_TMUX=1
| `Ctrl-b <arrow>` | Move between panes |
| `Ctrl-b d` | Detach (session stays alive) |
| `Ctrl-b r` | Reload tmux config |
| `F12` | Toggle mouse mode on/off (useful for mobile) |
| Mouse | Click to focus, scroll to scroll, drag to resize |

## Setup
Expand Down Expand Up @@ -206,13 +207,60 @@ Your project is mounted at `/workspace` inside the container.
### Bernard/Forge deployment
See [`DEPLOY.md`](./DEPLOY.md) for Caddy Docker Proxy integration. Uses `docker-compose.override.yml` (gitignored) for reverse proxy config that persists across git pulls.

## Session logging

Every agent session is automatically logged with structured events and a raw terminal transcript. Typing `claude`, `codex`, or `copilot` in any shell goes through `session-logger.sh` automatically.

```text
/workspace/.clide/logs/<session_id>/
events.jsonl — structured JSONL events (start, end, errors)
transcript.txt.gz — compressed raw terminal I/O
```

All logged output is scrubbed for secrets (API keys, tokens, passwords) before writing. See [`docs/schema/session-events-v1.md`](./docs/schema/session-events-v1.md) for the event format.

| Env var | Default | Description |
|---------|---------|-------------|
| `CLIDE_LOG_DISABLED` | _(empty)_ | Set to `1` to disable logging |
| `CLIDE_MAX_SESSIONS` | `30` | Max sessions retained (oldest pruned on new session) |

## Push notifications (ntfy)

Get notified when agent sessions start, end, or error. Works with any [ntfy](https://ntfy.sh) instance (self-hosted or public).

```env
# .env
CLIDE_NTFY_URL=https://ntfy.example.com
CLIDE_NTFY_TOPIC=clide
```

Subscribe to notifications on your phone via the ntfy app, or open `https://ntfy.example.com/clide` in a browser tab.

| Env var | Default | Description |
|---------|---------|-------------|
| `CLIDE_NTFY_URL` | _(empty)_ | ntfy server URL (notifications disabled if unset) |
| `CLIDE_NTFY_TOPIC` | `clide` | ntfy topic name |
| `CLIDE_NTFY_DISABLED` | _(empty)_ | Set to `1` to disable notifications |

## LAN CA certificate

If your internal services use TLS with a private CA (e.g. Caddy internal certs), the container can trust it at startup:

```env
# .env
CLIDE_CA_URL=https://fs.example.com/root-ca.crt
```

The cert is downloaded and installed on each container start. If the download fails, startup continues without it.

## Additional docs

| Doc | Contents |
|---|---|
| [`SECURITY.md`](./SECURITY.md) | Threat model, trust boundaries, attack surface, hardening recommendations |
| [`RUNBOOK.md`](./RUNBOOK.md) | Operational runbook — health checks, logs, rebuilds, credential rotation, troubleshooting |
| [`DEPLOY.md`](./DEPLOY.md) | Production deployment with Caddy reverse proxy |
| [`docs/schema/session-events-v1.md`](./docs/schema/session-events-v1.md) | Session event JSONL schema |

## Notes

Expand Down
25 changes: 23 additions & 2 deletions claude-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ set -euo pipefail
HOME_DIR="/home/clide"
export HOME="$HOME_DIR"

# Install LAN CA certificate at runtime if not already done (entrypoint.sh may have
# already handled this for the web service). Graceful — never blocks startup.
if [[ -n "${CLIDE_CA_URL:-}" && ! -f /usr/local/share/ca-certificates/lan-ca.crt ]]; then
if curl -fsSLk "${CLIDE_CA_URL}" -o /usr/local/share/ca-certificates/lan-ca.crt 2>/dev/null \
&& update-ca-certificates 2>/dev/null; then
echo "clide: installed CA cert from ${CLIDE_CA_URL}"
else
echo "clide: WARNING - failed to install CA cert from ${CLIDE_CA_URL}; continuing without it"
fi
fi

# Set up egress firewall (CLIDE_FIREWALL=0 to disable; CLIDE_ALLOWED_HOSTS to extend)
# Skip if a parent entrypoint already ran it for this container.
if [[ "${CLIDE_FIREWALL_DONE:-0}" != "1" ]]; then
Expand Down Expand Up @@ -137,8 +148,18 @@ fi
# Opt-in tmux wrapping for shell service (set CLIDE_TMUX=1 in .env)
# Web terminal always uses tmux via entrypoint.sh; this covers make shell / ./clide shell.
# Drop privileges to clide via gosu before exec so the workload never runs as root.

# Wrap agent CLIs with session logger for structured logging + transcript capture.
# Set CLIDE_LOG_DISABLED=1 to skip. Logger is agent-agnostic — works with claude, codex, etc.
AGENT_CMD="${*:-claude}"
if [[ -x /usr/local/bin/session-logger.sh && "${CLIDE_LOG_DISABLED:-}" != "1" ]]; then
AGENT_CMD="session-logger.sh ${AGENT_CMD}"
fi

if [[ -n "${CLIDE_TMUX:-}" ]]; then
exec gosu clide tmux new-session -A -s main "${@:-claude}"
# shellcheck disable=SC2086
exec gosu clide tmux new-session -A -s main ${AGENT_CMD}
fi

exec gosu clide "${@:-claude}"
# shellcheck disable=SC2086
exec gosu clide ${AGENT_CMD}
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ x-base: &base
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
GITLAB_TOKEN: ${GITLAB_TOKEN:-}
GITLAB_HOST: ${GITLAB_HOST:-}
# Session logging (v4: Agent Observability)
CLIDE_LOG_DISABLED: ${CLIDE_LOG_DISABLED:-}
CLIDE_MAX_SESSIONS: ${CLIDE_MAX_SESSIONS:-30}
# Push notifications via ntfy (v4: Agent Observability)
CLIDE_NTFY_URL: ${CLIDE_NTFY_URL:-}
CLIDE_NTFY_TOPIC: ${CLIDE_NTFY_TOPIC:-clide}
CLIDE_NTFY_DISABLED: ${CLIDE_NTFY_DISABLED:-}
# LAN CA certificate (e.g. Caddy internal TLS root)
CLIDE_CA_URL: ${CLIDE_CA_URL:-}
# Drop all capabilities then re-add only what entrypoint + firewall need.
# NET_ADMIN — iptables egress rules (set CLIDE_FIREWALL=0 to disable)
# SETUID/GID — gosu privilege drop from root → clide
Expand Down
77 changes: 77 additions & 0 deletions docs/schema/session-events-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Session Event Schema v1

All events are written as newline-delimited JSON (JSONL) to:
```
/workspace/.clide/logs/<session_id>/events.jsonl
```

Every event includes:
| Field | Type | Description |
|-------|------|-------------|
| `event` | string | Event type (see below) |
| `ts` | string | ISO 8601 timestamp (UTC) |
| `session_id` | string | `clide-<timestamp>-<random>` |
| `schema_version` | int | Always `1` |

## Event Types

### `session_start`
Emitted when an agent session begins.

| Field | Type | Description |
|-------|------|-------------|
| `agent` | string | `claude`, `codex`, `copilot`, or command name |
| `repo` | string | `owner/repo` from git remote |
| `model` | string | Model identifier |
| `command` | string | Full command (secrets scrubbed) |
| `cwd` | string | Working directory |

### `session_end`
Emitted when the agent session exits.

| Field | Type | Description |
|-------|------|-------------|
| `agent` | string | Same as session_start |
| `exit_code` | int | Process exit code |
| `outcome` | string | `success` or `error` |

## Session Directory Layout

```
/workspace/.clide/logs/<session_id>/
events.jsonl — structured event log
transcript.txt.gz — compressed raw terminal I/O
```

## Secret Scrubbing

All event payloads are scrubbed before writing:
1. Known secret env var values replaced with `[REDACTED:<NAME>]`
2. Heuristic: `KEY=longvalue` patterns replaced with `KEY=[REDACTED]`

Blocklist: `GH_TOKEN`, `GITHUB_TOKEN`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
`CLAUDE_CODE_OAUTH_TOKEN`, `TTYD_PASS`, `CLEM_WEB_SECRET`, `SUPERVISOR_SECRET`,
`TEDDY_API_KEY`, `TEDDY_WEB_PASSWORD`, `GITLAB_TOKEN`

## Retention

Configurable via `CLIDE_MAX_SESSIONS` (default: 30). Oldest sessions pruned
on each new session start.

## Notifications

Session start, end, and error events trigger push notifications via ntfy
when `CLIDE_NTFY_URL` is configured. Notifications are fire-and-forget
(failures are silent and never block the session).

## Configuration

| Env var | Default | Description |
|---------|---------|-------------|
| `CLIDE_LOG_DIR` | `/workspace/.clide/logs` | Log root directory |
| `CLIDE_MAX_SESSIONS` | `30` | Max sessions to retain |
| `CLIDE_LOG_DISABLED` | _(empty)_ | Set to `1` to disable logging |
| `CLIDE_NTFY_URL` | _(empty)_ | ntfy server URL |
| `CLIDE_NTFY_TOPIC` | `clide` | ntfy topic name |
| `CLIDE_NTFY_DISABLED` | _(empty)_ | Set to `1` to disable notifications |
| `CLIDE_CA_URL` | _(empty)_ | LAN CA certificate URL (installed at startup) |
19 changes: 18 additions & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
#!/bin/bash
set -e

# Install LAN CA certificate at runtime (e.g. Caddy internal TLS root).
# Set CLIDE_CA_URL in .env to the URL of your CA cert. Uses -k for the
# initial fetch since the cert isn't trusted yet. Graceful — never blocks startup.
if [[ -n "${CLIDE_CA_URL:-}" ]]; then
if curl -fsSLk "${CLIDE_CA_URL}" -o /usr/local/share/ca-certificates/lan-ca.crt 2>/dev/null \
&& update-ca-certificates 2>/dev/null; then
echo "clide: installed CA cert from ${CLIDE_CA_URL}"
else
echo "clide: WARNING - failed to install CA cert from ${CLIDE_CA_URL}; continuing without it"
fi
fi

# Pre-seed Claude config (auth, onboarding flags) — same as claude-entrypoint.sh
# This ensures CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY from .env are wired up
# before any shell session in the web terminal runs `claude`.
Expand All @@ -23,6 +35,7 @@ TTYD_ARGS=(
"--writable"
"--port" "${TTYD_PORT:-7681}"
"--base-path" "${TTYD_BASE_PATH:-/}"
"--client-option" "reconnect=${TTYD_RECONNECT:-3}"
)

# Wire gh as git credential helper so git push/fetch work without token embedding.
Expand Down Expand Up @@ -63,5 +76,9 @@ else
exit 1
fi

# Drop privileges to clide before starting ttyd so the web terminal never runs as root
# Drop privileges to clide before starting ttyd so the web terminal never runs as root.
# Note: ttyd logs the credential as base64 in its startup banner. This is only visible
# via `docker logs` (requires host access). We unset TTYD_PASS from the environment
# so child processes (tmux, shells, agents) can't read it.
unset TTYD_PASS
exec gosu clide ttyd "${TTYD_ARGS[@]}" tmux new-session -A -s main
Loading
Loading