Skip to content
Open
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Claudeman Environment Configuration
# Copy this file to .env and set your values

# HTTP Basic Auth password (required to enable authentication)
# If not set, the web interface is accessible without authentication
CLAUDEMAN_PASSWORD=your_secure_password_here

# HTTP Basic Auth username (optional, defaults to "admin")
# CLAUDEMAN_USERNAME=admin
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ Claudeman is a Claude Code session manager with a web interface and autonomous R
npm install
```

## Authentication

The web interface supports optional HTTP Basic Auth. To enable it:

1. Copy `.env.example` to `.env`
2. Set `CLAUDEMAN_PASSWORD` to your desired password
3. Optionally set `CLAUDEMAN_USERNAME` (defaults to "admin")
4. Restart the web server

```bash
cp .env.example .env
# Edit .env and set CLAUDEMAN_PASSWORD=your_secure_password
```

When enabled, browsers will prompt for credentials before accessing the web interface.

## Commands

**CRITICAL**: `npm run dev` runs CLI help, NOT the web server. Use `npx tsx src/index.ts web` for development.
Expand Down Expand Up @@ -470,6 +486,7 @@ All routes defined in `server.ts:buildServer()`. Key endpoint groups:

| File | Purpose |
|------|---------|
| `.env` | Environment config (CLAUDEMAN_PASSWORD, CLAUDEMAN_USERNAME for HTTP Basic Auth) |
| `~/.claudeman/state.json` | Full session state (all settings, tokens, respawn config, Ralph state), tasks, app config |
| `~/.claudeman/state-inner.json` | Ralph loop/todo state per session (separate to reduce writes) |
| `~/.claudeman/screens.json` | Screen session metadata (for recovery after restart) |
Expand Down
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@modelcontextprotocol/sdk": "^1.25.3",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"dotenv": "^17.2.3",
"fastify": "^5.1.0",
"ink": "^6.6.0",
"ink-spinner": "^5.0.0",
Expand Down
50 changes: 49 additions & 1 deletion src/web/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@
* - Server-Sent Events (SSE) for real-time updates at /api/events
* - Static file serving for the web UI
* - 60fps terminal streaming with batched updates
* - Optional HTTP Basic Auth (set CLAUDEMAN_PASSWORD in .env)
*
* @module web/server
*/

import Fastify, { FastifyInstance, FastifyReply } from 'fastify';
// Load environment variables from .env file (must be first)
import 'dotenv/config';

import Fastify, { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import fastifyStatic from '@fastify/static';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
Expand Down Expand Up @@ -94,6 +98,38 @@ const CLAUDE_BANNER_PATTERN = /\x1b\[1mClaud/;
const CTRL_L_PATTERN = /\x0c/g;
const LEADING_WHITESPACE_PATTERN = /^[\s\r\n]+/;

// HTTP Basic Auth credentials from environment
const AUTH_PASSWORD = process.env.CLAUDEMAN_PASSWORD || '';
const AUTH_USERNAME = process.env.CLAUDEMAN_USERNAME || 'admin';
const AUTH_ENABLED = AUTH_PASSWORD.length > 0;

/**
* Validates HTTP Basic Auth credentials.
* Returns true if auth is disabled or credentials are valid.
*/
function checkBasicAuth(req: FastifyRequest): boolean {
if (!AUTH_ENABLED) return true;

const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
return false;
}

try {
const base64Credentials = authHeader.slice(6);
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
const [username, password] = credentials.split(':');

// Constant-time comparison to prevent timing attacks
const usernameMatch = username === AUTH_USERNAME;
const passwordMatch = password === AUTH_PASSWORD;

return usernameMatch && passwordMatch;
} catch {
return false;
}
}

/**
* Sanitizes hook event data before broadcasting via SSE.
* Extracts only relevant fields and limits total size to prevent
Expand Down Expand Up @@ -286,6 +322,18 @@ export class WebServer extends EventEmitter {
}

private async setupRoutes(): Promise<void> {
// HTTP Basic Auth middleware (if CLAUDEMAN_PASSWORD is set in .env)
if (AUTH_ENABLED) {
console.log('[Server] HTTP Basic Auth enabled (CLAUDEMAN_PASSWORD is set)');
this.app.addHook('preHandler', async (req, reply) => {
if (!checkBasicAuth(req)) {
reply.header('WWW-Authenticate', 'Basic realm="Claudeman"');
reply.code(401).send({ error: 'Unauthorized' });
return;
}
});
}

// Serve static files
await this.app.register(fastifyStatic, {
root: join(__dirname, 'public'),
Expand Down