The open-source control plane for your personal AI workforce. Run your own AI agents. Your infrastructure. Your models. Your rules.
Timothy runs on your hardware and works for you around the clock. Shape agents with their own model, tools and knowledge, hand them real work, and let them chat, research, write code, read your inbox and calendar, and brief you about what matters. They remember who you are across every conversation and deliver results to your phone while you sleep. Every conversation, memory, document, and API key stays on infrastructure you control.
Use any model you want: Anthropic, OpenAI, Amazon Bedrock, GLM, a local Ollama, or any compatible endpoint. Route each kind of work to whichever model does it best, switch anytime from settings, no code changes, no lock-in.
Status: early, under active development.
Alpha releases with prebuilt images are available on the Releases page; expect rough edges and breaking changes between releases.
| Feature | What you get |
|---|---|
| One assistant, every model | Anthropic, OpenAI, Amazon Bedrock, local models via Ollama, or any compatible provider, all behind one interface. Pick which model handles chat, coding, research, or briefings, and let Timothy fail over to a backup when a provider has a bad day. |
| Give it real work | Hand Timothy a task (research a topic, write a report, fix a bug) and it works unattended: plans, executes, verifies its own output, and shows you the result with a full timeline of what it did. Quick tasks skip the ceremony and just get done. |
| Results find you | Any task or schedule can deliver its result to Telegram, email, a webhook, or GitHub (a pushed branch or an opened pull request) the moment it finishes, files attached. No checking a dashboard: the answer lands where you already are. |
| It writes code safely | Coding tasks run in isolated per-language sandboxes (Go, Node, Python, Java, PHP), on their own git branch, with the work verified before you see it. It can also hand the coding work to a CLI agent you already use, Claude Code, Codex, Cursor, opencode, or pi, while keeping review and budgets in your hands. |
| Your daily briefings | Wake up to a digest of your inbox, calendar, and spending, delivered to Telegram or email in your timezone, saying only what actually needs your attention. Schedule any task to run on your clock. |
| Connected to your life | Gmail, Google Calendar, Docs, Drive, GitHub, Outlook, IMAP, CalDAV, and any MCP server. Timothy reads them when a task needs it, and asks before doing anything destructive. |
| Shape your own assistants | Create named agents with their own personality, favorite model, and exactly the tools and knowledge they need, nothing more. A briefing agent that reads only your mail and calendar can never touch your code or send a message on your behalf. |
| It remembers you | Preferences, projects, and facts you share carry across conversations, and recurring patterns become insights over time. You approve what becomes a standing instruction; noise gets filtered before it ever reaches you. |
| Your documents, searchable | Drop in files or URLs; Timothy files them into topic collections and uses them to answer your questions. Your own knowledge base, on your own disk. |
| Nothing gets lost | Conversations survive restarts, crashes, and upgrades. Pick up any session exactly where it left off. |
| You control the spend | Every model call is priced and logged honestly. Set budgets with alerts, see exactly where the money goes, and route routine work to cheap or free models. |
| Private by design | Runs entirely on your hardware. Sensitive content like email can be pinned to a local model so it never leaves your network, and API keys live in an encrypted store (or your own Vault / AWS Secrets Manager), never in logs, never in the UI. |
| Talk to it | Optional voice input with fully local speech-to-text. Audio never leaves your machine. |
Things Timothy's own operator actually runs it for:
- 07:00, your phone buzzes. "Two things need you today: the client call at 14:00 has an unanswered thread from yesterday, and your card was charged twice by the same vendor. The other 14 emails were newsletters." A scheduled briefing read your inbox and calendar, cross-referenced them, and messaged you on Telegram.
- "Find every receipt from my Portugal trip and total it per currency." Timothy searches your Gmail, opens each receipt (never trusting a snippet), and reports an itemized breakdown with per-currency totals it computed with a calculator, not vibes.
- "Research the current EU AI Act timeline and write me a cited summary." It searches the web, reads primary sources, writes the report to a file, and a verification step checks the artifact exists and cites real URLs before you ever see "done".
- "Fix the flaky test in my repo." A coding mission clones the repo into a sandbox, works on its own branch, runs the tests, and opens the result for your review. Your laptop stays untouched.
- "Remember that Ana owes me EUR 200 from dinner, she'll pay in September." Weeks later: "Who owes me money?" answers correctly, because facts you tell it persist and stay retrievable.
- Drop a PDF into the knowledge base. It lands in the right topic collection automatically, and next week "what did that scaling article say about probabilistic counting?" quotes it back.
- Every evening at 20:00, an expense digest lists the day's spending from your inbox; every Monday at 07:00, a week-prep note cross-references your calendar with recent email threads and flags meetings that need preparation.
Each of these is a schedule, a chat message, or a one-line task. No plugins to write, no pipelines to build.
Go microservices behind a single public API, one PostgreSQL database, React web UI. All run via Docker Compose.
| Service | Role |
|---|---|
brain |
Public API: chat orchestration, agent loop, missions, event-sourced sessions, SSE streaming |
gateway |
Internal LLM gateway: multi-provider routing, cost ledger |
memoryd |
Internal memory service: pgvector-backed recall |
sandboxd |
Internal service holding the Docker socket: per-mission sandbox containers |
web |
React + Tailwind interface: chat, missions, usage, settings |
searxng |
Internal metasearch backend for the search_web tool |
markitdown |
Internal Python sidecar: file→markdown conversion |
whisper |
Internal Python sidecar: local speech-to-text for the web mic button (opt-in, off by default) |
pdfgen |
Internal Python sidecar: markdown→PDF via Typst, powers mission PDF export |
Plus Postgres (18 + pgvector), internal only, no host port. Migrations are embedded in each Go binary and applied automatically at startup; there's no separate migrate command. Every Go service exposes GET /health and GET /metrics; brain's /metrics, the only one on a published port, requires Authorization: Bearer $TIMOTHY_METRICS_TOKEN.
Sessions are an append-only event log: every turn, tool run, and compaction is an immutable event, so conversations survive crashes mid-stream and replay exactly as they happened.
Published ports (everything else is compose-internal):
| Port | What |
|---|---|
3300 |
Web UI |
8300 |
Brain (public API) |
3301 |
Vite dev server (make dev) |
Both published ports serve plain HTTP and are meant for a trusted LAN. For any exposure beyond that, put a reverse proxy in front that terminates TLS: the API token travels in an Authorization header on every request and is stored in the browser's localStorage, so over plain HTTP anyone on the path can read it. The web UI ships a Content-Security-Policy, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, and Referrer-Policy: strict-origin-when-cross-origin; a proxy that rewrites response headers should preserve them.
The fastest way to run Timothy: no Go/Node toolchain, no build step, just Docker and the released images.
curl -fsSL https://raw.githubusercontent.com/timothy-agent/timothy/main/deploy/release/install.sh | shThe installer resolves the newest release, installs into ~/timothy (override with TIMOTHY_HOME=/some/dir), generates a .env with fresh secrets (POSTGRES_PASSWORD, TIMOTHY_MASTER_KEY, TIMOTHY_API_TOKEN), pulls the images, starts the stack, and prints a magic sign-in link once the web UI is up. Open the link: the web UI signs in automatically.
Prefer to inspect scripts before running them? Every release also ships install.sh as an asset: download it from the releases page, read it, then sh install.sh.
Run the exact same command again:
curl -fsSL https://raw.githubusercontent.com/timothy-agent/timothy/main/deploy/release/install.sh | shThe installer finds your existing install (~/timothy, TIMOTHY_HOME, or the directory you run it from), keeps all your secrets, bumps TIMOTHY_VERSION to the newest release, refreshes docker-compose.yml, pulls the new images (including the mission sandbox), and restarts the stack. Your data lives in Docker volumes and your secrets in .env; neither is touched. Database migrations run automatically when the new version starts. Downgrading is not supported once a newer version's migrations have run.
The rest of this README covers building and running from source instead.
Prerequisites:
- Docker (Desktop, or engine + compose plugin).
-
Copy the env file and fill in the required values:
cp deploy/env.example deploy/.env
Open
deploy/.envand set:POSTGRES_PASSWORD: compose refuses to start without it.TIMOTHY_MASTER_KEY: generate withopenssl rand -base64 32. This is the root of trust for the encrypted secret store (provider API keys, OAuth tokens all live behind it). Compose hard-fails if it's blank. Back this up: losing it makes every stored secret unrecoverable.TIMOTHY_API_TOKEN: generate withopenssl rand -hex 32. Bearer token for the API; if it's blank, every request 401s.
-
Missions sandbox.
sandboxdis a required service:docker-compose.ymlfixes itsMISSION_SANDBOX_IMAGEattimothy-sandbox:latest, and it refuses to start without that image built.make upbuilds it for you, but you can build it ahead of time:make sandbox-image
This builds the base image plus the per-language variants (
timothy-sandbox-{go,node,python,java,php}:latest) that coding missions run in. -
(Linux only) Set
DOCKER_SOCK_GIDsosandboxdcan use the Docker socket:stat -c '%g' /var/run/docker.sockPut that number in
.env. On Docker Desktop the default of0works as-is. Note: mountingdocker.sockgivessandboxdroot-equivalent access to the host. It's isolated on its own compose network, read-only, and runs with all capabilities dropped, but the socket itself is the trust boundary, so only run this on a host you control. -
Start the stack:
make up
Web UI:
http://localhost:3300. API:http://localhost:8300. -
First login. There's no login page: the web UI auto-opens a settings dialog asking for an API token the first time it can't find one. Paste the
TIMOTHY_API_TOKENvalue fromdeploy/.env. It's stored in your browser'slocalStorage. -
Add a provider. A fresh install has zero LLM providers and no routing configured, so Timothy can't answer anything until you do this. Go to Settings → Providers, pick a preset tile (OpenAI, OpenAI Responses, Anthropic, Cursor, Bedrock, GLM, Grok, Ollama, or a custom OpenAI-compatible endpoint), fill in the form, and run the connection test before adding it. The API key you enter is encrypted into the secret store (default backend
db, encrypted withTIMOTHY_MASTER_KEY); the database only ever holds a reference to it, never the raw value, and it never appears in.env, logs, or API responses. Creating your first provider automatically bootstraps the 4 routes Timothy needs to work (default,summarize,embedding,vision); routes are otherwise fully user-managed (create, edit chain/strategy, delete) from Settings → Routing.
make up # start (builds images as needed)
make down # stop
make logs # follow logs for all servicesRebuild and restart a single service after a code change:
make brain # or gateway, memoryd, web, markitdown, whisper, pdfgen, sandboxdPostgres has no host port, so backups go through the container. scripts/backup-db.sh does that and writes a gzipped dump outside the pgdata volume:
scripts/backup-db.sh # writes ./backups/timothy-<UTC timestamp>.sql.gz
scripts/backup-db.sh /mnt/nas/timothy # or point it somewhere off-host| Variable | Default | Meaning |
|---|---|---|
BACKUP_DIR |
<repo>/backups |
Output directory; the first positional argument wins over it. |
BACKUP_KEEP |
14 |
How many dumps to keep. Rotation is by count, so a stack that was off for a month still keeps its last good dumps. |
The script takes no input, prints no secret values, and exits non-zero on any failure, so it runs unattended from cron:
15 3 * * * /path/to/timothy/scripts/backup-db.sh >> /var/log/timothy-backup.log 2>&1Each run dumps the whole database. Never narrow it to pg_dump -t <table> to save space: a table-scoped dump silently drops the secrets table, and the restore then comes up with every provider and connector credential missing. The script refuses to write a dump that has no secrets table for exactly this reason.
A complete backup is the dump plus TIMOTHY_MASTER_KEY from deploy/.env. The dump holds secrets only as ciphertext; without that key they are unrecoverable. Back the key up separately from the dumps, and never regenerate it: a new key orphans every secret already sealed with the old one.
-
Put the repo and
deploy/.envin place. The.envmust carry the sameTIMOTHY_MASTER_KEYas the instance the dump came from;POSTGRES_PASSWORDmay be new. -
Start Postgres alone, so no service writes while the restore is running:
docker compose -f deploy/docker-compose.yml up -d postgres
-
Load the dump.
ON_ERROR_STOP=1makes a partial restore fail loudly instead of leaving a half-populated database:gunzip -c backups/timothy-<timestamp>.sql.gz \ | docker compose -f deploy/docker-compose.yml exec -T postgres \ sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U timothy -d timothy -v ON_ERROR_STOP=1'
The dump recreates the schema, so restore into an empty database. If this Postgres already ran the stack once, drop and recreate first:
docker compose -f deploy/docker-compose.yml exec -T postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U timothy -d postgres -c "DROP DATABASE timothy" -c "CREATE DATABASE timothy"'. -
Check the data landed, including the secrets table:
docker compose -f deploy/docker-compose.yml exec -T postgres \ sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U timothy -d timothy -c "select count(*) from secrets" -c "select count(*) from session_events"'
-
Bring up the rest of the stack:
make up
-
Verify secrets actually decrypt, which is what proves the master key matches. Ask brain to run a provider connection test; it resolves the stored credential through the secret store:
curl -s -H "Authorization: Bearer $TIMOTHY_API_TOKEN" http://localhost:8300/v1/admin/providers curl -s -X POST -H "Authorization: Bearer $TIMOTHY_API_TOKEN" http://localhost:8300/v1/admin/providers/<id>/test
A successful test means the ciphertext decrypted. A decrypt failure in
make logs(gateway or brain) means theTIMOTHY_MASTER_KEYindeploy/.envis not the one that sealed these rows; restore the correct key rather than re-entering credentials, or every historical secret stays unreadable.
git pull
make upMigrations are additive-only, never edited once applied, and run automatically at service startup; no separate migrate step.
The Go toolchain runs fully containerized; no host Go install required.
make build # compile everything
make test # unit tests
make vet # go vet
make lint # golangci-lintFrontend development with hot reload:
make dev # Vite dev server on :3301, proxies /v1 to brainmake test-integration, make canary, and make kb-eval (retrieval eval harness, scripts/kb-eval/) need the compose stack up (make up first).
Design decisions are documented as D-0XX markers in code comments next to the code they explain.
