Skip to main content

opendot
An interactive terminal AI agent you can fully undo.

CI PyPI Downloads Python versions License: MIT

opendot demo


opendot works directly on your real files and shell — but unlike other terminal agents, every action it takes is snapshotted first, so you can see exactly what it did and cleanly walk it back. Files and shell commands, not just in-repo edits. Commands whose effects escape your workspace (network, sudo, git push, deleting outside the working dir) are flagged and confirmed before they run, with an honest note about what can't be undone.

That's the point of opendot: an agent you can let loose because nothing it does is a surprise, and (almost) nothing is irreversible.

opendot is model-agnostic — it works with any model through LiteLLM (OpenAI, Anthropic, Google, DeepSeek, …) and runs fully local via Ollama. Ollama is just the zero-setup local option; use whatever backend you prefer.

Installation

# try it instantly, no install
uvx opendot

# recommended (isolated global CLI)
uv tool install opendot        # or: pipx install opendot

# also works
pip install opendot

Use

opendot                              # open an interactive chat
opendot -p "summarize this project"  # one-shot, for scripts / CI
opendot --model claude-opus-4-5      # launch with a specific model (see below)

opendot log                          # audit: what has the agent done here?
opendot undo                         # revert the last action
opendot undo 000004                  # restore the workspace to before action #4
opendot redo                         # re-apply the last thing you undid
opendot diff 000004                  # dry-run: preview what undo-ing action #4 would change
opendot resume                       # continue this project's previous session

Inside the chat, slash-commands: /model (searchable model picker), /provider (connect a provider + paste an API key), /mcp, /composio, /log, /undo, /redo, /diff, /trace (per-model-call cost + timing), /clear, /compact, /help.

Any model

Any model works — cloud, local, or Hugging Face. You need an API key for the provider you want to use (opendot is BYO-key; it doesn't host models). Pick a model and paste a key right inside the chat with /model and /provider, or set the key in your environment and pass --model:

Provider names link to where you get a key.

Provider Env var Example --model
OpenAI OPENAI_API_KEY gpt-5.1
Anthropic ANTHROPIC_API_KEY claude-opus-4-5
Google GEMINI_API_KEY gemini/gemini-3-pro
DeepSeek DEEPSEEK_API_KEY deepseek/deepseek-chat
Groq GROQ_API_KEY groq/llama-3.3-70b-versatile
Hugging Face HF_TOKEN huggingface/together/deepseek-ai/DeepSeek-R1
Ollama (local, no key) ollama/qwen3

Reasoning models stream their thinking live.

Local OpenAI-compatible servers (llama.cpp / llama-server, vLLM, LM Studio): point opendot at the server with --api-base and an openai/-prefixed model.

# e.g. llama.cpp: llama-server -m model.gguf --port 8080
opendot --model openai/local --api-base http://localhost:8080/v1

Which model runs. The default is gpt-5.1. If its key (OPENAI_API_KEY) isn't set but another provider's key is, opendot automatically switches to that provider on launch — e.g. with only DEEPSEEK_API_KEY set, a bare opendot uses deepseek/deepseek-chat. If no provider key is found, opendot starts fine but the first message shows a hint to set a key or run /provider (rather than a raw provider error). ollama/* models need no key — just a local Ollama.

Budget cap

Same idea as reversibility, one axis over: reversibility bounds the damage a run can do, a budget bounds the spend. Set a ceiling and opendot stops the run the moment accumulated cost or tokens cross it, instead of running to the step limit.

opendot -p "refactor this module" --usd 0.50    # stop at 50 cents of spend
opendot -p "..." --tokens 100000                # stop at 100k tokens

Use --usd when the model has known pricing; use --tokens for a self-hosted --api-base model whose rates aren't known (a local model prices at $0, so a token cap is the meaningful one there). The defaults come from OPENDOT_MAX_USD / OPENDOT_MAX_TOKENS if set; an invalid or non-positive value is ignored, and unset means unlimited (the default). When a cap is hit the run ends with a clear budget exceeded / token limit exceeded message.

Unattended runs & permissions

By default opendot confirms every irreversible or workspace-escaping action (and one-shot runs decline them, since there's no one to ask). For CI or an unattended run, a permission policy lets you decide up front what's allowed:

opendot -p "run the test suite and fix failures" --yes         # auto-approve prompts
opendot -p "..." --yes --deny "git push" --deny "rm -rf"       # but hard-block these
opendot -p "..." --allow "pytest"                              # approve only these
  • --yes auto-approves anything that would otherwise prompt. Reversibility is unchanged: every action is still snapshotted and undoable.
  • --allow PATTERN / --deny PATTERN match the action (the command or path) on word boundaries — so --deny rm matches the rm command, not "reformat" — or as a glob when the pattern contains */? (e.g. --deny "git push*", --deny "*.env"). Both are repeatable.
  • Precedence is deny > allow > (--yes ? approve : ask) — so you can auto-approve broadly and still guarantee specific things never run.

You can pin the same rules per project in OPENDOT.md (see below), so they travel with the repo; CLI flags merge on top.

Sandbox mode (--sandbox)

The permission policy is a heuristic gate, not a security boundary (a script it runs can still reach outside the workspace). For an unattended run where that matters, --sandbox moves the guarantee to a kernel-enforced boundary: it runs the turn inside a container against a copy of the workspace, then commits only the resulting diff back on success.

opendot -p "refactor and run the tests" --sandbox        # run isolated in a container
opendot -p "..." --sandbox --sandbox-net                 # allow network (off by default)
  • Needs docker or podman; if neither is installed, --sandbox errors — it never silently falls back to a direct run.
  • Network is off by default inside the container; only the API key your model needs is forwarded, nothing else from your environment.
  • On success the workspace diff is committed back through the reversibility engine (snapshotted and undoable, subject to the snapshot size limit for very large files); on a non-zero container exit nothing is applied.
  • One-shot only (-p) — an interactive session inside a headless container isn't useful. Your normal, direct terminal runs are unchanged when you don't pass it.

The container image (--sandbox-image, default python:3.12-slim) must have opendot available. This is the strongest boundary opendot offers; the classifier remains the explainer for direct runs.

Connect MCP servers

opendot is an MCP client: connect any MCP server and its tools become available to the agent alongside the built-in ones. Manage them from inside the chat with /mcp (a dropdown of your servers and their status, with "➕ Add a server"), or from the command line:

# a stdio server — put its launch command after `--`
opendot mcp add <name> --env KEY=VALUE -- <command> [args...]

# a remote server (http/sse)
opendot mcp add <name> --url <https url>

# a remote server with a static token — pass an HTTP header
opendot mcp add supabase \
  --url "https://mcp.supabase.com/mcp?project_ref=<id>&read_only=true" \
  --header "Authorization=Bearer <your-supabase-access-token>"

# a remote server that uses OAuth — authorize in your browser
opendot mcp add linear --url "https://mcp.linear.app/mcp" --oauth

opendot mcp list           # show configured servers
opendot mcp remove <name>  # remove one

Servers are stored in ~/.opendot/mcp.json and connect automatically on the next launch; connected servers appear in the sidebar. Authenticated remote servers work two ways: a static token via --header (e.g. Supabase's access token), or browser OAuth via --oauth (or by typing oauth in the /mcp add form) — opendot opens your browser to authorize, runs a one-shot local callback server to catch the redirect, and caches the issued tokens under ~/.opendot/mcp_oauth/ (owner-readable only), refreshing them automatically. Removing a server also forgets its cached OAuth tokens.

Because opendot can't know what an external tool does, every MCP tool call is treated as irreversible — it's confirmed before running and marked ✗ in the ledger. Your built-in file/shell actions stay snapshotted and undoable as usual.

Connect apps with Composio

Beyond MCP, opendot can connect to Composio's 1000+ app tools (Gmail, Slack, GitHub, Notion, Linear, …) using your own Composio API key. Just use /composio in the chat:

  • The first /composio asks for your Composio API key (stored in ~/.opendot/composio.json, owner-readable only).
  • After that, /composio lists the available apps. Pick one — if it needs OAuth, opendot opens your browser to authorize and waits for you to finish; direct/API-key connectors activate immediately.
  • Enabled apps appear in the sidebar; their tools load on the next launch.

Composio tools reach external services, so — like MCP — every call is treated as irreversible: confirmed first, marked ✗ in the ledger.

Project rules — OPENDOT.md

Drop an OPENDOT.md in your project. Its prose is given to the agent as context. You can also control what gets snapshotted with an opendot block:

```opendot
# snapshot these even though they'd normally be skipped:
snapshot: dist
# never snapshot these:
skip: data, *.log
# permission policy (same as --allow/--deny; comma-separated, may contain spaces):
allow: pytest, ruff
deny: git push, rm -rf
```

By default opendot skips .git, node_modules, virtualenvs, and build caches when snapshotting — your rules override those in either direction. The allow: / deny: lists set the project's permission policy; the --allow / --deny CLI flags merge on top of them.

How the reversibility works

  • Before every file write or shell command, opendot snapshots the working directory into a content-addressed store in ~/.opendot (each unique file stored once, so snapshots are cheap).
  • Every action is recorded in an append-only ledger you can inspect with opendot log, which shows a timeline with a ▸ you are here cursor — so you can see at a glance which actions are applied and which have been undone (and are still redoable).
  • opendot undo restores the workspace to a chosen point, exactly, and opendot redo re-applies what you last undid (a wrong undo is itself reversible).
  • opendot diff is a dry run: it shows exactly which files an undo or restore would change, before you commit to it.
  • Sessions persist per project, so opendot resume picks up the previous conversation and its ledger where you left off.
  • A conservative classifier decides which shell commands are workspace- contained (auto-run, undoable) vs. escaping (confirmed first, marked irreversible). It classifies each command in a chain independently (so a safe a && b can't smuggle a dangerous b past the prompt) and treats opaque interpreters (python, bash, docker, …) as confirm-first, since a script can do anything. When unsure, it asks. Built-in file writes are additionally contained at the OS level so a symlinked path can't redirect them outside the workspace.

Honest boundary: opendot cannot undo effects that leave your machine (a sent email, a dropped remote database, a git push). It tells you before running those, rather than pretending otherwise.

The classifier is a heuristic that decides when to ask, not a security boundary. It reads shell text, so it can't fully account for what an opaque subprocess does (the script run by python foo.py is as unknowable as python -c), and a determined adversary could race a symlink swap. That's why interpreters are confirm-first and built-in file writes are contained at open time — but the honest framing is that the classifier explains why a confirmation is prudent; it is not a guarantee against hostile input. Kernel-enforced isolation (an overlay/container for unattended runs) would be the stronger boundary, and is planned but not yet built (#130).

Skipping the snapshot on purpose. When opendot runs a shell command it snapshots first — but for something you want gone (securely wiping a secret) or a huge throwaway file, that snapshot would keep a recoverable copy in the store. Prefixing the command opendot runs with OPENDOT_NO_SNAPSHOT=1 skips the snapshot for that one command:

OPENDOT_NO_SNAPSHOT=1 shred secrets.txt

The action is still logged for the audit trail, but marked not-undoable (no snapshot backs it). This only affects commands opendot itself runs; anything you run in your own shell outside opendot is never snapshotted or logged either way. To exclude paths from snapshotting permanently, use the skip: rule in OPENDOT.md.

Tool output cap. Tool results are truncated so one huge file can't blow the context. Set OPENDOT_MAX_TOOL_OUTPUT to change the per-tool character cap (default 30000); non-integer or non-positive values keep the default.

Office files

With the optional office extra installed, the agent can read and edit spreadsheets and documents directly:

uv tool install "opendot[office]"   # or: pip install "opendot[office]"

This adds tools for .xlsx (read a sheet, edit a cell, append rows, create a sheet), .pptx (read slides), and .docx (read paragraphs). Spreadsheet edits go through the same snapshot-first path as any other file write, so they're undoable like everything else.

Contributing

Issues and PRs welcome — see CONTRIBUTING.md for setup and the one hard rule (don't break reversibility). Security reports go through SECURITY.md.

git clone https://github.com/vedaant00/opendot
cd opendot
uv pip install -e ".[dev]"   # or: pip install -e ".[dev]"
pytest

Status

Early (alpha). The interactive agent, local tools, and the full reversibility engine (undo, redo, diff preview, and a you are here timeline) work and are tested. Streaming, slash-commands, OPENDOT.md rules, per-project session resume, spend/token budgets, a permission policy (--yes / allow-deny) for unattended runs, an end-of-session summary, MCP and Composio connectors, and office (.xlsx/.pptx/.docx) tools are in. A richer TUI and more tools are coming.

MIT licensed.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

opendot-0.4.0.tar.gz (1.8 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

opendot-0.4.0-py3-none-any.whl (119.1 kB view details)

Uploaded Python 3

File details

Details for the file opendot-0.4.0.tar.gz.

File metadata

  • Download URL: opendot-0.4.0.tar.gz
  • Upload date:
  • Size: 1.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for opendot-0.4.0.tar.gz
Algorithm Hash digest
SHA256 b916420b134290ce293cfb4974ddb48031f99156e6c978c43d1879ee37e6c226
MD5 c17822a07778d199402d5edc910fb823
BLAKE2b-256 5f97e0f1a80e4feed39738b1fb4d1a91674d28ce3c5985cf75d6d66bd1e8cc52

See more details on using hashes here.

Provenance

The following attestation bundles were made for opendot-0.4.0.tar.gz:

Publisher: release.yml on vedaant00/opendot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file opendot-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: opendot-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 119.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for opendot-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 52a47862187a924c4d29bc3d6f6311e8040c7f08333c1338751f0b3a4621d901
MD5 b5f9cc9102f6a1a448d6b79e7a24d5eb
BLAKE2b-256 6aa20606181def14c04dc77212bdb849441156e9fbc14141f1e615796d17ebd5

See more details on using hashes here.

Provenance

The following attestation bundles were made for opendot-0.4.0-py3-none-any.whl:

Publisher: release.yml on vedaant00/opendot

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.2

2 files

0.4.1

2 files

This release

0.4.0 This release

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.9

2 files

0.2.8

2 files

0.2.7

2 files

0.2.6

2 files

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

0.0.9

2 files

0.0.8

2 files

0.0.7

2 files

0.0.6

2 files

0.0.5

2 files

0.0.4

2 files

0.0.3

2 files

0.0.2

2 files

0.0.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page