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
12 changes: 6 additions & 6 deletions .claude/skills/add-gmail/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Ask the user:
>
> **Option 1: Tool Mode**
> - Agent can read and send emails when you ask it to
> - Triggered only from WhatsApp (e.g., "@Andy check my email" or "@Andy send an email to...")
> - Triggered only from WhatsApp (e.g., "@GroupGuard check my email" or "@GroupGuard send an email to...")
> - Simpler setup, no email polling
>
> **Option 2: Channel Mode**
Expand Down Expand Up @@ -261,11 +261,11 @@ Tell the user:

> Gmail integration is set up! Test it by sending this message in your WhatsApp main channel:
>
> `@Andy check my recent emails`
> `@GroupGuard check my recent emails`
>
> Or:
>
> `@Andy list my Gmail labels`
> `@GroupGuard list my Gmail labels`

Watch the logs for any errors:

Expand Down Expand Up @@ -295,7 +295,7 @@ Ask the user:
> - Uses Gmail's plus-addressing feature
>
> **Option C: Subject Prefix**
> - Emails with a subject starting with a keyword (e.g., "[Andy]")
> - Emails with a subject starting with a keyword (e.g., "[GroupGuard]")
> - Anyone can trigger the agent by using the prefix

Also ask:
Expand All @@ -318,7 +318,7 @@ Store their choices for implementation.

### Step 1: Complete Tool Mode First

Complete all Tool Mode steps above before continuing. Verify Gmail tools work by having the user test `@Andy check my recent emails`.
Complete all Tool Mode steps above before continuing. Verify Gmail tools work by having the user test `@GroupGuard check my recent emails`.

### Step 2: Add Email Polling Configuration

Expand All @@ -344,7 +344,7 @@ export const EMAIL_CHANNEL: EmailChannelConfig = {
triggerValue: 'NanoClaw', // the label name, address pattern, or prefix
contextMode: 'thread',
pollIntervalMs: 60000, // Check every minute
replyPrefix: '[Andy] '
replyPrefix: '[GroupGuard] '
};
```

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/customize/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist

User: "Add Telegram as an input channel"

1. Ask: "Should Telegram use the same @Andy trigger, or a different one?"
1. Ask: "Should Telegram use the same @GroupGuard trigger, or a different one?"
2. Ask: "Should Telegram messages create separate conversation contexts, or share with WhatsApp groups?"
3. Find Telegram MCP or library
4. Add connection handling in index.ts
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ This step configures three things at once: the trigger word, the main channel ty
### 6a. Ask for trigger word

Ask the user:
> What trigger word do you want to use? (default: `Andy`)
> What trigger word do you want to use? (default: `GroupGuard`)
>
> In group chats, messages starting with `@TriggerWord` will be sent to Claude.
> In your main channel (and optionally solo chats), no prefix is needed — all messages are processed.
Expand Down Expand Up @@ -227,8 +227,8 @@ mkdir -p data

Then write `data/registered_groups.json` with the correct JID, trigger, and timestamp.

If the user chose a name other than `Andy`, also update:
1. `groups/global/CLAUDE.md` - Change "# Andy" and "You are Andy" to the new name
If the user chose a name other than `GroupGuard`, also update:
1. `groups/global/CLAUDE.md` - Change "# GroupGuard" and "You are GroupGuard" to the new name
2. `groups/main/CLAUDE.md` - Same changes at the top

Ensure the groups folder exists:
Expand Down
8 changes: 4 additions & 4 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Core code - maintainer only
/src/ @gavrielc
/container/ @gavrielc
/package.json @gavrielc
/package-lock.json @gavrielc
/src/ @TomGranot
/container/ @TomGranot
/package.json @TomGranot
/package-lock.json @TomGranot

# Skills - open to contributors
/.claude/skills/
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2026 Gavriel
Copyright (c) 2026 Tom Granot

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
158 changes: 74 additions & 84 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
# GroupGuard

WhatsApp group moderation powered by Claude. Automated content filtering, spam prevention, and natural-language admin controls — all running in isolated containers.
WhatsApp group moderation powered by Claude.

Built on [NanoClaw](https://github.com/gavrielc/nanoclaw).
## Quick Start

```bash
git clone git@github.com:TomGranot/groupguard.git
cd groupguard
./setup.sh
```

**Requirements:** Node.js 20+, macOS 26+ (Apple Containers) or Docker, [Claude Code](https://claude.ai/download)

## What It Does

GroupGuard sits in your WhatsApp groups and enforces rules automatically. Messages that violate rules get deleted instantly, and the sender gets a private explanation. Admins control everything through natural language — just tell the bot what you want.
GroupGuard sits in your WhatsApp groups and enforces rules automatically. Messages that violate rules get deleted instantly, and the sender gets a private explanation. Admins control everything through natural language.

```
@GroupGuard enable no-spam and no-links for Family Chat
@GroupGuard set observation mode for Work Team (log violations but don't delete)
@GroupGuard enable no-spam and no-links for this group
@GroupGuard set observation mode (log violations but don't delete)
@GroupGuard show moderation stats for the last week
@GroupGuard disable quiet-hours for the Main group
@GroupGuard add a keyword filter blocking "crypto" and "forex"
```

Beyond moderation, it's a full Claude assistant — it can answer questions, search the web, schedule tasks, and manage files. The moderation just runs silently in the background.
Beyond moderation, it's a full Claude assistant — it can answer questions, search the web, schedule tasks, and manage files. The moderation runs silently in the background.

## Guards

Expand Down Expand Up @@ -47,7 +55,7 @@ Beyond moderation, it's a full Claude assistant — it can answer questions, sea
| `quiet-hours` | Block during certain hours | `startHour` (22), `endHour` (7) |
| `approved-senders` | Whitelist-only mode | `allowedJids` |

Guards run on the host process, not inside the container — enforcement is instant. Messages blocked by guards never reach the database.
Guards run on the host process, not inside the container — enforcement is instant.

## How Moderation Works

Expand All @@ -73,7 +81,7 @@ Observation mode? --> Yes: log violation, store message anyway
Delete message + DM sender with reason + log violation
```

Each group has independent guard configurations and a moderation config:
Each group has independent guard configurations:

```json
{
Expand All @@ -89,142 +97,124 @@ Each group has independent guard configurations and a moderation config:
}
```

- **Observation mode**: Log violations without deleting — useful for testing rules before enforcing
- **Observation mode**: Log violations without deleting — useful for testing rules
- **Admin exempt**: Group admins bypass all guards
- **DM cooldown**: Prevent notification spam (one DM per user per 60s)
- **DM cooldown**: One notification per user per 60s to prevent spam

All violations are logged to SQLite with timestamp, sender, guard ID, action, and reason.

## Quick Start

```bash
git clone git@github.com:TomGranot/groupguard.git
cd groupguard
./setup.sh
```

Or use Claude Code for guided setup: run `claude` then `/setup`.

**Requirements:** Node.js 20+, Docker, [Claude Code](https://claude.ai/download) (for API key)

## Architecture

```
WhatsApp (baileys) --> Guard filter --> SQLite --> Polling loop --> Docker (Claude Agent SDK) --> Response
```

Single Node.js process. Moderation runs on the host for instant enforcement. Agent responses run in isolated Docker containers with mounted directories. Per-group message queues. IPC via filesystem.

Key files:
- `src/index.ts` — Main app: WhatsApp connection, message routing, IPC
- `src/moderator.ts` — Guard evaluation, DM enforcement, admin caching
- `src/guards/` — Guard implementations (content, property, behavioral, keyword)
- `src/container-runner.ts` — Spawns streaming agent containers
- `src/task-scheduler.ts` — Runs scheduled tasks
- `src/db.ts` — SQLite operations (messages, moderation logs, groups, sessions)
- `groups/*/CLAUDE.md` — Per-group memory

## Features

- **14 moderation guards** — Content filtering, spam prevention, rate limiting, keyword blocking
- **Observation mode** — Test rules without enforcing them
- **WhatsApp I/O** — Message Claude from your phone, manage groups naturally
- **Isolated group context** — Each group has its own memory, filesystem, and container sandbox
- **Main channel** — Your private admin control channel with elevated privileges
- **Scheduled tasks** — Recurring jobs that run Claude and can message you back
- **Web access** — Search and fetch content
- **Container isolation** — Agents sandboxed in Docker containers (macOS/Linux)
- **Moderation logging** — Full audit trail in SQLite

## Usage

Talk to your bot with the trigger word (default: `@GroupGuard`):

```
@GroupGuard enable no-spam for this group
@GroupGuard show me the last 10 moderation violations
@GroupGuard add a keyword filter blocking "crypto" and "forex"
@GroupGuard schedule a daily summary of moderation activity at 9am
@GroupGuard schedule a daily summary at 9am
@GroupGuard what's the weather in Tel Aviv?
```

From the main channel, you have admin control over all groups:
```
@GroupGuard list all groups and their guard configs
@GroupGuard enable observation mode for Work Team
@GroupGuard show moderation stats across all groups
@GroupGuard register the "Family Chat" group
```

## Deploying to a Server

GroupGuard needs Docker to spawn agent containers, which rules out most managed platforms — you need a real VM.
GroupGuard needs a container runtime (Docker or Apple Containers) to spawn agent containers.

### Option 1: One-Click Deploy (exe.dev) — $20/month
### Local macOS (free)

The fastest path. [exe.dev](https://exe.dev) gives you a VM with Docker pre-installed and an AI agent that sets everything up.
Just run `./setup.sh`. It auto-detects Apple Containers (macOS 26+) or Docker Desktop and installs a launchd service that starts on boot.

After the VM is provisioned (~5 min), authenticate WhatsApp:
### Hetzner VPS ($4/mo, always-on)

```bash
ssh <vm-name>.exe.xyz
cd /opt/groupguard && npm run auth # scan QR code with your phone
sudo systemctl start groupguard # start the service
```

### Option 2: Budget VPS (Hetzner) — ~$4/month

Best value. [Hetzner Cloud](https://www.hetzner.com/cloud/) with dedicated resources.
Best value for an always-on server.

1. Create a server: **CX22** (2 vCPU, 4 GB RAM, 40 GB disk), Docker CE app image, Ubuntu 24.04
1. Create a [Hetzner Cloud](https://www.hetzner.com/cloud/) server: **CX22** (2 vCPU, 4 GB RAM), Docker CE app image, Ubuntu 24.04
2. SSH in and run:

```bash
# Install Node.js
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc && nvm install 22

# Clone and setup
git clone git@github.com:TomGranot/groupguard.git /opt/groupguard
cd /opt/groupguard
echo 'ANTHROPIC_API_KEY=your-key-here' > .env
./setup.sh

npm run auth # scan QR code
sudo systemctl start groupguard # start the service
# Authenticate WhatsApp (scan QR code)
npm run auth
sudo systemctl start groupguard
```

### Other Options
### Other VPS Options

| Provider | Cost | Notes |
|----------|------|-------|
| **DigitalOcean** | $6-12/mo | Docker 1-Click image |
| **Vultr** | $6-10/mo | Startup scripts |
| **Linode/Akamai** | $5/mo+ | StackScripts |
| **Oracle Cloud** | Free | ARM A1 (hard to provision) |
| **Local macOS** | Free | Docker Desktop + launchd via `./setup.sh` |

## Troubleshooting
### Updating

- **Docker not running** — macOS: start Docker Desktop. Linux: `sudo systemctl start docker`
- **WhatsApp auth expired** — Run `npm run auth` to re-authenticate, then restart
- **Service not starting** — Check `logs/nanoclaw.log` and `logs/nanoclaw.error.log`
- **No response to messages** — Check the trigger pattern, verify the group is registered
- **Guards not working** — Check moderation logs: `sqlite3 store/messages.db "SELECT * FROM moderation_log ORDER BY timestamp DESC LIMIT 10"`
- **Container networking on macOS** — Docker Desktop handles this automatically. If using colima/lima, run `sudo ./scripts/macos-networking.sh`
```bash
cd /opt/groupguard
git pull
npm install
npm run build
./container/build.sh
sudo systemctl restart groupguard # or launchctl on macOS
```

Run `/debug` in Claude Code for guided troubleshooting.
## Architecture

```
WhatsApp (baileys) --> Guard filter --> SQLite --> Polling loop --> Container (Claude Agent SDK) --> Response
```

Single Node.js process. Moderation runs on the host for instant enforcement. Agent responses run in isolated containers with mounted directories. Per-group message queues. IPC via filesystem.

| File | Purpose |
|------|---------|
| `src/index.ts` | Main app: WhatsApp connection, message routing, IPC |
| `src/moderator.ts` | Guard evaluation, DM enforcement, admin caching |
| `src/guards/` | Guard implementations (content, property, behavioral, keyword) |
| `src/container-runner.ts` | Spawns containers with runtime detection (Docker/Apple) |
| `src/task-scheduler.ts` | Runs scheduled tasks |
| `src/db.ts` | SQLite operations (messages, moderation logs, tasks) |
| `groups/*/CLAUDE.md` | Per-group memory (isolated) |

## Customizing

The codebase is small enough to modify safely. Tell Claude Code what you want:

- "Add a new guard that blocks messages with more than 3 emojis"
- "Change the DM message format when a message is blocked"
- "Add a daily moderation report that gets sent to the admin group"
- "Add a daily moderation report sent to the admin group"

Or run `/customize` for guided changes.

## Based On
## Troubleshooting

GroupGuard is built on [NanoClaw](https://github.com/gavrielc/nanoclaw), a lightweight personal Claude assistant. NanoClaw provides the core architecture (WhatsApp connection, container isolation, scheduling, IPC) and GroupGuard adds the moderation layer on top.
- **Container runtime not running** — macOS: start Docker Desktop or ensure Apple Containers is available. Linux: `sudo systemctl start docker`
- **WhatsApp auth expired** — Run `npm run auth` to re-authenticate, then restart
- **Service not starting** — Check `logs/nanoclaw.log` and `logs/nanoclaw.error.log`
- **No response to messages** — Check the trigger pattern, verify the group is registered
- **Guards not working** — Check logs: `sqlite3 store/messages.db "SELECT * FROM moderation_log ORDER BY timestamp DESC LIMIT 10"`
- **Container networking on macOS** — Docker Desktop handles this automatically. For Apple Containers or colima/lima, run `sudo ./scripts/macos-networking.sh`

Run `/debug` in Claude Code for guided troubleshooting.

## License

MIT

---

Built on [NanoClaw](https://github.com/gavrielc/nanoclaw).
4 changes: 2 additions & 2 deletions container/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ RUN npm run build
RUN mkdir -p /workspace/group /workspace/global /workspace/extra /workspace/ipc/messages /workspace/ipc/tasks

# Create entrypoint script
# Sources env from mounted /workspace/env-dir/env if it exists
RUN printf '#!/bin/bash\nset -e\n[ -f /workspace/env-dir/env ] && export $(cat /workspace/env-dir/env | xargs)\ncat > /tmp/input.json\nnode /app/dist/index.js < /tmp/input.json\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
# Secrets are passed via stdin JSON, not via env files
RUN printf '#!/bin/bash\nset -e\ncat > /tmp/input.json\nnode /app/dist/index.js < /tmp/input.json\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh

# Set ownership to node user (non-root) for writable directories
RUN chown -R node:node /workspace
Expand Down
Loading