Note
TinyCTX is in Beta. Interfaces, config keys, and internal storage formats may still change between versions. Back up your instance directory before upgrading.
A context-efficient agentic assistant framework. Connect it to your LLM, configure a bridge (CLI, Discord, Telegram, Matrix, or HTTP gateway), and you have a persistent, tool-using AI agent with memory consolidation, a knowledge graph, a capability-based permission system, scheduled cron jobs, subagent support, and web browsing.
- Effortless onboarding wizard
- Optimised for local LLMs - 32k context recommended, 16k workable
- Branch-backed conversation tree persisted in SQLite - no state lost on restart
- Active memory consolidation and background knowledge-graph extraction (LadybugDB)
- Semantic search over your notes: BM25 or hybrid BM25 + embeddings
- Named-capability permission system, checked centrally against a declarative per-tool/per-command requirement
- Terminal UI with persistent session restore, slash commands, paste refs, and copy helpers
- Web browsing via
web_search,open_url, and Camoufox-based browser automation (click, type, extract, screenshot) - Cron-scheduled agent turns, backed by a SQLite store outside the agent's own filesystem reach
- Subagent support (
spawn_agent/wait_agent) - Tool discovery: BM25 + vector search over the tool registry, with passive auto-enable and an explicit
tools_searchtool - MCP server integration
- Provider presets for OpenAI, OpenRouter, Ollama, LM Studio, llama.cpp, and any OpenAI-compatible endpoint
Warning
Security notice - read before exposing to a network.
TinyCTX gives the agent real tools: shell execution, file read/write, and web access. Any user who can reach the bot can instruct the agent to use these tools. By default, bridges accept messages from everyone.
Before enabling any network bridge (Discord, Telegram, Matrix, gateway), decide who is allowed to talk to the bot and configure accordingly:
allowed_servers/room_ids/allowed_users- restrict which servers, rooms, or users the bot responds to. An emptyallowed_serversmeans the bot won't respond in any server.- Capability-based permissions - each user's authority is a set of named capabilities (file read/write, network read/write, shell execution location, cron creation, and so on), not a single number. Tools and slash commands each declare the capabilities they require; a caller missing any of them is denied. A single shared
permissions.templateinconfig.yamlsets the default grant for every user; grant more to specific users viapermission_overrides. prefix_required: true- in group channels, only respond when @mentioned or prefixed. This reduces noise but is not a security boundary on its own.- Gateway
api_key- always set a strong, random key if the gateway is enabled. Never expose the gateway port to the public internet without authentication.
The filesystem module sandboxes write_file/edit_file to the workspace directory only. view/grep/glob_search can additionally see any directory listed in filesystem.read_only_paths in config.yaml (read-only, never writable - e.g. /app for the agent's own source code in the container). Nothing outside workspace + that whitelist is reachable at all. The shell tool runs in an isolated sandbox container by default (no LAN access); running it in the main container instead requires the backend_exec capability. Commands are parsed with tree-sitter-bash, classified into the capability bools they need, and checked centrally - a structural shape policy underneath rejects $(), unquoted globs used as commands, and unrecognized bash syntax as a last-resort guardrail, not a substitute for access control.
The right mental model: treat TinyCTX like an SSH session. Only give access to people you'd give a shell to.
git clone https://github.com/itzpingcat/TinyCTX
cd TinyCTX
pip install -e .
python -m TinyCTX onboardThis starts the interactive configuration wizard. It will walk you through choosing a provider, configuring your workspace, and optionally setting up bridges.
An instance is a self-contained directory holding one agent's config, workspace, and internal data - everything an agent needs lives in one place, so running multiple agents is just running multiple instance directories.
Every tinyctx command resolves the instance directory the same way:
--dir PATH, if given- The nearest ancestor of your current directory that's literally named
.tinyctx(so running from inside<instance>/workspace/skills/foostill resolves correctly) - A
.tinyctx/child of your current directory - Fallback:
~/.tinyctx
<instance>/
+-- config.yaml # loaded from here by default
+-- workspace/ # agent-authored content - visible to the agent's own filesystem tools
| +-- SOUL.md # Agent personality - loaded first, every turn
| +-- AGENTS.md # Sub-agent or persona definitions
| +-- TOOLS.md # Tool usage guidelines
| +-- EM.md # Equipment manifest (optional; templated with OS/date/paths)
| +-- rag/, memory/*.md # Semantic search corpus - any *.md files here are searchable
| +-- downloads/ # Files and images sent by users via bridges
| +-- outputs/browser/ # Camoufox screenshots
| +-- skills/
| +-- mytool/
| +-- SKILL.md
+-- data/ # TinyCTX-internal state - NOT visible to the agent's own filesystem tools
+-- agent.db # Branch-backed conversation tree (SQLite WAL)
+-- users.db # User registry and permission overrides
+-- cron.db # Scheduled jobs (cron module)
+-- cursors/ # Per-bridge/session cursors (CLI resume uses this)
+-- memory/
+-- graph.lbug # LadybugDB knowledge graph (memory module)
+-- librarian.log # Librarian logging
+-- dedup_cache.db
workspace.path and data.path both default to <instance>/workspace and <instance>/data - relative to wherever config.yaml itself lives - so a fresh config.yaml doesn't need to state either explicitly. Override only if you want something non-standard.
Edit files under workspace/ any time - they are re-read every turn, no restart needed. Files under data/ are internal state the agent's own tools can't reach; edit them only if you know what you're doing.
TinyCTX does not keep chat state only in RAM. Conversations are stored in data/agent.db as a branch tree, and the CLI bridge restores the visible transcript from the saved cursor on startup.
TinyCTX is designed to work within a fixed context window rather than silently discarding history. Set context: in config.yaml to match your model:
context: 32768 # recommended; 16384 works for smaller modelsWhen the active turn approaches this limit, TinyCTX trims the oldest non-system turns. The memory and RAG modules then pick up the slack - important facts are preserved in the knowledge graph or semantic index and re-injected as needed. The full conversation tree is always on disk.
TinyCTX has three complementary memory systems.
These files are always injected every turn:
SOUL.md- agent personalityAGENTS.md- roles, personas, or sub-agent definitionsTOOLS.md- tool usage guidelines
Any .md files placed under workspace/rag are indexed. They can be configured to be searched automatically each turn. The most relevant chunks are injected into context. Subdirectories are supported.
To enable embedding-based (semantic) search, add an embedding model:
models:
embed:
kind: embedding
base_url: http://localhost:11434/v1
api_key_env: N/A
model: nomic-embed-text
memory_search:
embedding_model: embedWithout an embedding model, BM25 keyword search is used - no extra server required.
The agent can also call rag_search explicitly to look things up on demand. See example.config.yaml under rag: for all options (chunk strategy, budget, top-k, auto-inject, etc.).
The memory module adds a property-graph knowledge store backed by LadybugDB. A background librarian process walks unvisited conversation nodes (tracked with DB flags), extracts entities and relationships via sub-agents, and writes them to data/memory/graph.lbug. The main agent reads the graph via kg_search, kg_traverse, and call_librarian tools. Pinned entities are injected into the system prompt automatically.
# memory module (all optional - these are the defaults)
# graph_path: data/memory/graph.lbug # resolved relative to data.path, not workspace.path
# trigger_interval_hours: 6
# batch_size: 20
# embedding_model: "" # empty = keyword-only graph search
# memory_block_tokens: 4096
# librarian_model: "" # empty = use primary LLMEvery inbound message is associated with a User - a TinyCTX-internal account that may have identities on multiple platforms (Discord, Telegram, Matrix, CLI, etc). Users are created automatically on first contact and stored in <instance>/data/users.db.
Authority is a set of named capabilities, not a single number. Each capability is a narrow, independently gate-able bool: file_read, file_write, network_read, network_write, backend_exec (run tools in the main container instead of the sandbox), untrusted_exec, manage_ctx, model_swap, memory_read, memory_write, cron_create, cron_admin, user_read, root, dm_access, equipment_trusted, and image_gen. root is a deliberate catch-all meaning "administer the instance itself" (edit anyone's permissions, rename or merge users, shut the gateway down) - it does not imply the other capabilities, so grant it explicitly alongside anything else a fully-elevated user needs.
Tools and slash commands each declare the capability set they require. A caller is only allowed to invoke a tool or command if their effective capability set covers what it declares; the check runs centrally, once, before execution. Network capabilities are split by effect rather than byte direction - network_write covers anything with a durable remote effect (POST/PUT/PATCH/DELETE-shaped calls) and implies network_read, since any such call also returns a response body.
A single permissions.template in config.yaml sets the default grant shared by every user (omitting it defaults to fully closed). Grant more to a specific user with permission_overrides on their record, via /user-style admin commands, onboard/fix_permissions.py, or the gateway's user-elevation endpoint.
By default (permissions.minimal_tokens: true), the LLM only sees tools the current caller has permission to execute - higher-privilege tools are hidden entirely, saving tokens and avoiding confusion. Set minimal_tokens: false to show all tools; execution-time guards still apply.
Beyond visibility filtering, TinyCTX ranks the tool registry itself so large tool sets don't have to sit fully expanded in context. Every registered tool is embedded (BM25 always; vector search when an embedding model is configured) and re-synced each turn against a content hash, so unchanged tools cost nothing to re-rank. Two paths use the same ranking:
- Passive search - runs automatically each turn and silently enables the top-scoring tools for the model before it sees its tool list.
tools_search- an explicit tool the model can call to list candidates matching a query; a matching tool must still be enabled with a follow-up call before it can be invoked.
Both fuse BM25 and vector results via reciprocal-rank fusion. See example.config.yaml under tools: for auto-enable limits, score thresholds, and the embedding model used for tool ranking.
Skills are reusable instruction sets the agent can load on demand. Place a folder containing a SKILL.md file anywhere under workspace/skills/.
The agent sees a compact index of available skills and calls use_skill("name") to load the full instructions when needed. Skills follow the agentskills.io convention.
TinyCTX can spawn bounded child branches for parallel side work:
spawn_agent(prompt="...") - start a detached subagent
wait_agent(task_id="...") - wait for it to finish or poll status
Good for isolated side tasks; not worth the overhead for trivial work you can finish in the current turn.
The cron module lets the agent schedule its own future turns. Jobs are stored in a SQLite database under the instance's internal data dir (data/cron.db) rather than in workspace/, so the agent's own filesystem tools cannot create or edit jobs directly. A schedule is a single cron expression (evaluated via croniter); a one-shot reminder is just a cron expression matching one specific future minute rather than a separate schedule shape.
Jobs are created, listed, and removed only through the add_cron, list_cron, and remove_cron tools:
add_cronruns as the calling user's real identity and channel, and requires the caller holdcron_create. A job only fires if its stored creator currently holdscron_create- this is re-checked every run, not just at creation, so revoking the capability later disables the job's future runs outright rather than letting it run half-privileged.list_cron/remove_crononly see or act on jobs created in the caller's own channel;remove_cronadditionally requires being the job's creator, or holdingcron_admin.
Job output is delivered back to the originating channel through the same per-platform renderer a live turn uses.
cron:
store_file: cron.db # relative to data.pathAny stdio MCP server can be wired in via config:
mcp:
servers:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
tools:
read_file: always_on
write_file: deferred
delete_file: disabledPer-tool visibility: always_on | deferred (hidden until tools_search enables it) | disabled. All tools default to deferred if no tools: block is present.
Two complementary tools:
web_search- DuckDuckGo search.open_url- browser-renders a page and returns elements, text, HTML, or a screenshot.
Browser automation (click, type, extract, screenshot) runs on Camoufox, a Firefox build hardened against fingerprinting and bot detection. One Camoufox instance is shared per agent session across all browser tools. By default it runs headful under Xvfb (headless: "virtual" in config) rather than plain headless, since plain headless Firefox is easily detected by bot checks; set headless: true for genuine headless, or false to show a real window. Screenshots are saved under workspace/outputs/browser/ and returned inline when under screenshot_max_bytes.
web:
headless: virtual # true | false | "virtual"
timeout_ms: 30000
screenshot_max_bytes: 1500000
search_results: 5| Tool | What it does |
|---|---|
shell |
Run a shell command; sandboxed by default, or in the main container with backend_access=True (requires backend_exec) |
view |
Read a file with line numbers, or list a directory |
write_file |
Create or write to a file (append, prepend, overwrite) |
edit_file |
Edit an existing file by replacing a string |
grep |
Search file contents with regex (ripgrep with Python fallback) |
glob_search |
Find files by name pattern, sorted by modification time |
web_search |
Search the web via DuckDuckGo |
open_url |
Open any URL in a Camoufox browser; returns elements/text/html/screenshot |
memory_search |
Search the RAG semantic index |
kg_search |
Search the LadybugDB knowledge graph |
kg_traverse |
Traverse graph relationships from a starting entity |
call_librarian |
Trigger on-demand knowledge-graph extraction |
spawn_agent |
Start a detached subagent on a child branch |
wait_agent |
Wait for a spawned subagent to finish or poll its status |
add_cron |
Schedule a future agent turn (recurring or one-shot) |
list_cron |
List cron jobs in the caller's own channel |
remove_cron |
Remove a cron job the caller created (or any job, with cron_admin) |
use_skill |
Load a skill by name |
todo_write |
Update the session task checklist |
todo_read |
View the current task list |
present |
Deliver files to the user via the active bridge |
tools_search |
BM25 + vector search over available tools; enables matching deferred tools |
Write tools (write_file, edit_file) require the file to have been read first via view() - this prevents blind overwrites.
Full annotated config: see example.config.yaml. Key top-level keys:
| Key | Default | Purpose |
|---|---|---|
context |
16384 |
Token budget (recommend 32768) |
max_tool_cycles |
10 |
Max tool-call iterations per turn |
workspace.path |
<instance>/workspace |
Agent-visible working directory |
data.path |
<instance>/data |
Internal state (agent.db, users.db, cron.db, memory graph, cursors) - not visible to the agent's own filesystem tools |
filesystem.read_only_paths |
[] |
Extra directories view/grep/glob_search can see (never write to) - e.g. /app |
llm.primary |
- | Primary model name (must be kind: chat) |
llm.fallback |
[] |
Fallback model names, tried in order |
permissions.template |
fully closed | The capability set every user gets by default |
permissions.minimal_tokens |
true |
Hide tools the caller cannot use |
web.headless |
"virtual" |
Camoufox display mode: true | false | "virtual" |
cron.store_file |
cron.db |
Cron job store, relative to data.path |
gateway.api_key |
- | Auth token for the HTTP gateway |
gateway.port |
8085 |
Overridable per-instance via TINYCTX_PORT env (set automatically by tinyctx start) |
Models are defined under models: with kind: chat (default) or kind: embedding. Embedding models are never used for LLM routing.
Interactive terminal session with Rich TUI, slash commands, and persistent session restore.
tinyctx launch clibridges:
discord:
enabled: true
options:
token_env: DISCORD_BOT_TOKEN
allowed_servers:
987654321098765432: [] # all channels in this server
dm_enabled: true
prefix_required: true
command_prefix: "!"Required bot intents: Message Content, Server Members. Required permissions: Read Messages, Send Messages, Read Message History. Who may DM the bot or /reset in a server is governed by the dm_access and manage_ctx capabilities on the caller, not a separate config threshold.
bridges:
telegram:
enabled: true
options:
token_env: TELEGRAM_BOT_TOKEN # env var holding the @BotFather token
allowed_users: [123456789] # Telegram user IDs; empty = open to all
max_reply_length: 4096 # Telegram's hard per-message limit
mention_aliases: ["eve"] # extra names the bot answers to in groupsCreate a bot with @BotFather and export the token as TELEGRAM_BOT_TOKEN. Run the bridge like the others: python -m TinyCTX.bridges.telegram. In groups the bot answers to its @username, its BotFather display name, a reply to one of its own messages, and any mention_aliases; bare-name matching requires privacy mode disabled in @BotFather (/setprivacy, then Disable, then re-add the bot to the group).
bridges:
matrix:
enabled: true
options:
homeserver: https://matrix.org
username: "@yourbot:matrix.org"
password_env: MATRIX_PASSWORD
allowed_users: ["@you:matrix.org"]Requires matrix-nio (pip install matrix-nio, or matrix-nio[e2e] for E2EE).
OpenAI-compatible /v1/chat endpoint with SSE streaming. Useful for external clients and SillyTavern.
gateway:
enabled: true
host: 127.0.0.1
port: 8085
api_key: "your-secret-token"tinyctx onboard # first-run setup wizard for the resolved instance
tinyctx start # start the stack (docker compose) for the resolved instance
tinyctx stop # stop it
tinyctx status # check if running
tinyctx launch cli # attach an interactive terminal sessionAll five accept --dir PATH to target a specific .tinyctx instance directory instead of relying on autodetection - this is how you run more than one agent on the same machine: give each its own instance directory (e.g. tinyctx onboard --dir ~/agent-b/.tinyctx), then use --dir (or cd into it) for every subsequent command against that instance.