Madz is an AI harness designed to learn, adapt, and assist. You speak to it through a terminal interface, a one-shot command, or a pipe into your scripts. It does not demand ceremony. It demands clarity.
This guide takes you from zero to your first conversation — and then to working on your projects.
Before we build, we must prepare the ground. Ensure your system meets these requirements:
Docker packages an application and all its dependencies into a single, isolated container. It ensures madz runs identically on your machine, day after day, without conflicts or "it works on my machine."
If you're new to Docker: Do not worry. The commands below are straightforward, and each part is explained.
Before you run anything, it helps to have a mental model of the two "places" involved:
./memory, ./skills, ~/projects./app. madz runs inside the container; you talk to it over SSH.-v ./memory:/app/memory means "make the host's ./memory directory appear at /app/memory inside the container." Changes on either side are visible on the other.The key idea: the container is disposable, your data is not. When you pull a new image or recreate the container, anything written inside the container's filesystem is lost. Anything in a bind mount or named volume survives. That's why the setup mounts memory/ and skills/ from your host — so your profile, memories, and skills persist across container upgrades.
The Docker container is the deployment model — for home use and professional use alike. It runs locally, with no authentication and no server. You reach it over SSH on a loopback port.
📦 Just want to run it? (Minimal Setup)
mkdir -p ./memory ./skills
echo 'OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxx' > .env
docker run -d --name madz -p 2222:22 -v ./memory:/app/memory -v madz-checkpoints:/app/memory/checkpoints -v ./skills:/app/skills --env-file .env avoidwork/madz:latest
docker ps # confirm the container is Up
ssh -p 2222 madz@localhost
This creates the data directories, writes your key to .env, starts the container, verifies it, and connects you. For full configuration and bind mount explanations, see below.
Step 1: Pull the official image
docker pull avoidwork/madz:latest
This downloads the pre-built application package from Docker Hub to your local machine.
Step 2: Prepare your data directories
Bind mounts link a directory on your host machine directly into the container. This ensures your conversation history, memory files, and custom skills survive container restarts, upgrades, and removals.
Why externalize data? Docker containers are ephemeral by design. They can be stopped, replaced, or destroyed during updates, migrations, or troubleshooting. Any data written inside the container's filesystem is lost when the container is removed. By mounting host directories into the container, we externalize persistent state so it survives the container lifecycle. When you pull a new image or recreate the container, your memories and skills remain intact.
mkdir -p ./memory ./skills
SQLite checkpoints: madz uses an SQLite-backed checkpointer for LangGraph state persistence. The checkpoint database lives in memory/checkpoints/checkpoints.db. Rather than bind-mounting this subdirectory to the host, use a Docker named volume. This keeps checkpoint data managed by Docker — no host-side directory needed, and the DB survives container recreation without touching local files.
Step 3: Run the container
docker run -d \
--name madz \
-p 2222:22 \
-v ./memory:/app/memory \
-v madz-checkpoints:/app/memory/checkpoints \
-v ./skills:/app/skills \
--env-file .env \
avoidwork/madz:latest
Security Note: Avoid passing API keys directly via -e flags, as they will persist in your shell history. Instead, create a .env file in your project root with your variables and reference it with --env-file .env. For quick testing, you can still use -e OPENAI_API_KEY="your-key" directly, but remember to switch to .env for anything beyond a trial.
Example .env files:
OpenAI (Cloud):
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxx
OPENAI_MODEL=gpt-4o
OPENAI_BASE_URL=https://api.openai.com/v1
Ollama (Local):
OPENAI_BASE_URL=http://host.docker.internal:11434/v1
OPENAI_MODEL=gemma4:12b
# OPENAI_API_KEY is optional for local providers
Custom model with explicit encoding:
OPENAI_BASE_URL=http://your.inference.lan:8000/v1
OPENAI_MODEL=Qwen/Qwen3-32B
OPENAI_ENCODING=qwen2_base
When using a non-OpenAI model, you may need to set OPENAI_ENCODING to specify the tiktoken encoder name that matches the model's tokenizer. See the Encoding Reference in the README for common mappings.
Note for Docker users: host.docker.internal is required so the container can reach the Ollama service running on your host machine. If you're on Linux, you may need to use --network host instead.
Flag breakdown:
| Flag | Purpose |
|---|---|
-d | Run in detached mode (background) |
--name madz | Assign a human-readable name to the container |
-p 2222:22 | Map host port 2222 to container SSH port 22 (avoids conflicts with local SSH) |
-v ./memory:/app/memory | Bind mount host ./memory into container /app/memory for persistence |
-v madz-checkpoints:/app/memory/checkpoints | Named volume for SQLite checkpoint data (managed by Docker, no host directory needed) |
-v ./skills:/app/skills | Bind mount host ./skills into container /app/skills for custom tools |
--env-file .env | Inject sensitive credentials securely from a local file |
Volumes vs. Bind Mounts: Docker supports two persistence methods. Volumes are managed by Docker and live in /var/lib/docker/volumes/. Bind mounts (used here) link directly to a path on your host filesystem. We use bind mounts for memory/ and skills/ so you can read, edit, and version-control your memory and skills files directly from your terminal or editor. SQLite checkpoint data (memory/checkpoints/) uses a named volume instead — it's a binary database that doesn't need host-side editing, and Docker manages its lifecycle automatically.
Port collision? If port 2222 is already in use, change the host port in the -p flag (e.g., -p 2223:22) and update your SSH command accordingly.
Verify it's alive:
docker ps # STATUS should read "Up"
docker logs madz # boot log: scheduler sync, session init, TUI ready
The container is disposable; your data is not. To upgrade:
docker pull avoidwork/madz:latest
docker rm madz
docker run -d \
--name madz \
-p 2222:22 \
-v ./memory:/app/memory \
-v madz-checkpoints:/app/memory/checkpoints \
-v ./skills:/app/skills \
--env-file .env \
avoidwork/madz:latest
Same flags, new image. Because memory/, skills/, and the checkpoint volume live outside the container, your profile, memories, sessions, and skills are exactly where you left them.
madz reads its configuration from config.yaml. Sensitive values should be injected via environment variables to keep secrets out of version control. Secrets belong in the dark. Configuration belongs in the light.
madz is architecturally designed for local AI. While it supports cloud endpoints, its core philosophy prioritizes data sovereignty and privacy. By running models locally, you keep your conversation history, memory files, and custom skills entirely on your machine. No telemetry. No external data routing. Just pure, unfiltered compute.
Any OpenAI-compatible endpoint works. madz talks to its model through the OpenAI chat-completions API, so it works with any provider that exposes that interface — not just OpenAI itself. That includes:
ollama pull gemma4:12b), and point OPENAI_BASE_URL at it.You point madz at one by setting OPENAI_BASE_URL (and OPENAI_MODEL) in your .env. For local providers, OPENAI_API_KEY is optional — many run without authentication.
Cloud providers (OpenAI, OpenRouter, and others) are fully supported via the configuration below if latency or model availability dictates it, but the architecture assumes local-first by default.
Config keys map to UPPER_SNAKE_CASE environment variables. Container-specific keys (providers, credentials, timeout, search) are stripped from the variable name.
| Config Path | Environment Variable | Default |
|---|---|---|
providers.openai.credentials.apiKey | OPENAI_API_KEY | (required) |
providers.openai.model | OPENAI_MODEL | gpt-4o |
providers.openai.base_url | OPENAI_BASE_URL | https://api.openai.com/v1 |
providers.openai.temperature | OPENAI_TEMPERATURE | 0.4 |
providers.openai.maxTokens | OPENAI_MAX_TOKENS | 4096 |
providers.openai.rateLimit.requestsPerMinute | OPENAI_REQUESTS_PER_MINUTE | 120 |
providers.openrouter.apiKey | OPENROUTER_API_KEY | (empty) |
providers.openrouter.model | OPENROUTER_MODEL | openrouter/auto |
providers.fal.credentials.apiKey | FAL_API_KEY | (empty) |
sandbox.timeout.seconds | SANDBOX_TIMEOUT_SECONDS | 600 |
sandbox.timeout.gracePeriod | SANDBOX_GRACE_PERIOD | 5 |
sandbox.maxReadSize | SANDBOX_MAX_READ_SIZE | 10mb |
Note: The table reflects the shipped
config.yamldefaults, which differ from the schema defaults in a few places. The schema default forsandbox.timeout.secondsis30(shipped:600), forsandbox.maxReadSizeis1mb(shipped:10mb), and forproviders.openai.rateLimit.requestsPerMinuteis60(shipped:120). If you override any of these via env var, the value is a plain number (seconds, requests) or a size string (e.g.,10mb,500kb).
You can also reference environment variables directly in config.yaml:
providers:
openai:
credentials:
apiKey: "${OPENAI_API_KEY}"
If you deployed with Docker (the deployment model), connect to the container using the SSH port you passed to docker run. The container's SSH daemon listens on internal port 22, so you map it to a host port of your choosing (commonly 2222 to avoid conflicts with your local SSH):
ssh -p 2222 madz@localhost
The madz user has no password. On login the TUI launches automatically. Type /exit (or /quit) to leave; Esc interrupts a running response. When madz exits, the SSH session terminates with it — SSH login drops you straight into the TUI, not a shell. If you need a shell for inspection, use docker exec -it madz /bin/sh from the host instead (see Troubleshooting).
First command to try: Give me a quick system health check — CPU load, memory, and disk.
On your very first run, madz will detect that no user profile exists and initiate an interactive onboarding flow. It will ask a series of targeted questions to build your initial profile (e.g., "What do you build?", "What tools do you use?", "How direct should I be?"), establishing a foundation for deep, immediate personalization.
This profile is saved to memory/context/profile.md and injected into the system prompt at the start of every session, ensuring consistent, tailored behavior from day one.
Over time, madz autonomously captures ephemeral memories during operation. These entries log interaction patterns, decision milestones, and stylistic preferences, layering directly onto the base profile. The system prompt is dynamically rebuilt each session, ensuring consistent, context-aware behavior without manual intervention.
To re-trigger the initial profile setup, simply delete memory/context/profile.md and restart.
This is where madz stops being a demo and becomes a teammate. The container ships a full development toolchain — Node.js, Python, Ruby, Go, Java, Rust, Terraform, git, the GitHub CLI, and three package managers — so it can build, test, and commit real code.
Step 1: Mount your projects directory.
Add one bind mount to your docker run command, pointing at the directory on your host that holds your repositories:
docker run -d \
--name madz \
-p 2222:22 \
-v ./memory:/app/memory \
-v madz-checkpoints:/app/memory/checkpoints \
-v ./skills:/app/skills \
-v ~/projects:/app/projects \
--env-file .env \
avoidwork/madz:latest
Your repositories are now visible inside the container at /app/projects/<repo>. The image already ships an empty /app/projects/ directory (owned by the madz user), so the mount simply overlays it — no mkdir required.
Step 2: The convention is built in.
You do not need to configure anything for this to work. The system prompt carries a standing directive:
Project directories are cwd. Each immediate subdirectory of
projects/is a standalone project root. When the user works on one, treat that subdirectory as the working directory for the task — resolve relative paths against it, run commands with it as the working directory, and apply its own project rules (AGENTS.md, config, lint) instead of the parent's.
So naming a project is enough. The agent resolves the name to its directory and works there.
Step 3: Speak in direct statements.
Out of the box, you no longer describe paths. You describe work:
Fix the failing test in backend-api.
Run the build in my photo-app repo and tell me what broke.
Create a PR on backend-api for the session timeout fix.
The agent resolves the project name to its directory and works there — reading files, running commands, editing, committing, and pushing, all inside the mounted repo.
Optional: reinforce or adjust the convention with a memory.
The built-in directive covers the standard case. If you mount projects somewhere other than /app/projects, or you want to add project-specific rules the agent should always honor, layer a canonical memory on top. Ask in plain language:
Remember: my projects live in /app/projects. When I name a project,
treat /app/projects/<name> as the working root — resolve all file
operations, git commands, and builds relative to it.
madz writes this to memory/context/ and loads it into every session. You can also create the file yourself:
# Projects
My projects live in `/app/projects`. When I refer to a project by name
(e.g. "backend-api"), treat `/app/projects/backend-api` as the working
root — resolve all file operations, git commands, and builds relative to it.
Project conventions: If a repository contains an AGENTS.md, the agent discovers and follows it — commit format, lint rules, branch policy. Your project's own rules take precedence over general behavior.
Semantic code search: For larger codebases, index a project so the agent can search it by meaning, not just by keyword. Configure the project in .env — the project name is the first segment, lowercased, so keep it a single word:
VECTOR_PROJECTS_BACKENDAPI_ROOT_DIR=/app/projects/backend-api
VECTOR_PROJECTS_BACKENDAPI_DB_PATH=/app/projects/backend-api/vector.db
(include defaults to src/**/*.js, src/**/*.mjs, src/**/*.cjs; add VECTOR_PROJECTS_BACKENDAPI_INCLUDE_0=... to change it. Array fields are indexed from _0, so VECTOR_PROJECTS_BACKENDAPI_INCLUDE_0=src/**/*.ts replaces the first include pattern.) Then ask:
Index the code in backendapi for vector search
The agent's indexing tool reads these from the environment — no file editing required. (.env is injected at container start, so after adding these lines, recreate the container with the same docker run flags.)
Verified: The
VECTOR_PROJECTS_*env vars are materialized bysyncEnv()insrc/config/loader.js, which resolves dynamic record keys undervector.projects. SettingVECTOR_PROJECTS_BACKENDAPI_ROOT_DIRandVECTOR_PROJECTS_BACKENDAPI_DB_PATHcreates abackendapiproject entry with the defaultinclude/exclude/chunkSize/chunkOverlap/maxFileSizevalues. Array overrides likeVECTOR_PROJECTS_BACKENDAPI_INCLUDE_0replace the corresponding index.
Set the active project explicitly.
The convention above lets the agent resolve a project by name. But sometimes you want to pin the working directory — so the file picker and the agent's file operations both target a specific project. Use /projects:
/projects
This opens the projects panel, listing every directory under projects/. Navigate with ↑/↓, press Enter to select one, and it becomes the active project. The status bar shows the selected project's name (e.g. [backend-api]), and the @ file picker now globs that project's directory instead of the container root.
Once a project is active, the @ file picker lists files from inside it:
read @src/config/loader.js
The picker searches the active project's directory, so @src/... resolves relative to the project root — no need to type the full path.
Clear the active project.
To return to the default working directory (the container root), clear it:
/projects clear
Or select (clear active project) at the top of the projects panel. The status bar stops showing a project, and the @ file picker globs the container root again.
Once inside the interactive terminal, use these commands:
| Command | Action |
|---|---|
↑ / ↓ | Scroll conversation history |
/help | List available commands |
/quit | Exit the application |
/exit | Exit the application |
/provider set <name> | Switch LLM provider |
/config set <path> <value> | Mutate config at runtime |
/schedule list | List all scheduled jobs |
/schedule pause <name> | Pause a scheduled job |
/schedule resume <name> | Resume a paused job |
/schedule run-now <name> | Run a job immediately |
/gc | Trigger V8 garbage collection |
/gc status | Check GC availability and call count |
/clear | Clear current conversation |
/new | Start a fresh session |
/sessions | Open the sessions panel |
/memories | Open the memories panel |
/skills | Open the skills panel |
/subagents | Open the subagents panel — view each subagent's reasoning, message segments, and tool calls live during a multi-agent turn |
/settings | Open the settings panel |
/projects | Open the projects panel — select a project directory to set the active project |
/projects clear | Clear the active project back to the default |
Ctrl+F | Toggle in-conversation search |
Ctrl+R | Toggle reasoning block collapse/expand |
Ctrl+T | Toggle tool-call result block collapse/expand |
madz operates on a triple-layer memory architecture:
.md files in memory/context/. Loaded into every session. Includes profile, clarifications, reflections, and temporal captures.expiresAt frontmatter field.0 2 * * *) that runs the reflection skill in chat mode. Stored as canonical memories in memory/context/ with createdDate and updatedDate metadata. The job file is ensured at startup by the scheduler's crontab sync and persisted as memory/schedules/reflection-daily.json.This triple-layer architecture powers the autonomous learning loop — canonical memories persist, ephemeral memories capture moments, and reflections synthesize patterns into lasting context.
Changes to canonical memory require a /new command to refresh the current session context.
Skills are how you give madz new capabilities — a bit like a macro in Excel, but with more intention. You define a set of instructions, and madz follows them whenever a task matches. Skills let you package domain expertise, repeatable workflows, and specialized tools that madz can discover and invoke on demand.
Creating skills is natural. Just ask madz in everyday language — it will generate the necessary files for you. You can also create them manually in the skills/ directory.
Each skill is a folder containing a SKILL.md file with YAML frontmatter and instructions. When you ask madz to perform a related task, it matches your request to the skill description, loads the full instructions, and executes — optionally running any bundled scripts, loading reference documents, or applying templates.
Directory structure:
my-skill/
├── SKILL.md # Required: metadata + instructions
├── scripts/ # Optional: executable code
├── references/ # Optional: documentation
└── assets/ # Optional: templates, resources
Example SKILL.md:
---
name: system-info
description: Retrieve current system metrics, uptime, and memory usage. Use when the user asks about system health, load, or performance.
license: MIT
---
1. Run `uptime` and `free -h`.
2. Format the output into a concise summary.
3. Report back to the user.
Skills are stored in skills/ and are version-controllable. Simple skills can be chained together into pipelines for complex multi-step processing, or composed by asking madz to coordinate between them.
Built-in tools: Beyond skills, madz ships with built-in tools for common tasks. The Deep Agents orchestrator (deepagents library) handles multi-agent routing natively — see Delegating Work to Subagents. The scanAgents tool scans for AGENTS.md workspace rules files. Other built-in tools include filesystem operations, shell execution, search, memory management, and more.
madz supports cron-based scheduled jobs that run in non-interactive mode. Each invocation inherits the current session's memory context and sandbox permissions. Max-concurrency control prevents run overlap.
To schedule a task, simply ask:
madz, schedule the news-email skill to run during the week at 8pm
madz will parse the natural language instruction and create the cron entry for you. Manage jobs from the TUI with /schedule list, /schedule pause <name>, /schedule resume <name>, and /schedule run-now <name>.
For headless execution, use chat mode — the response streams to stdout, so you can pipe it into other tools:
node index.js "Summarize memory/_index.md"
This is the same mode the internal cron jobs use (e.g., node index.js --message "Run the reflection skill"). In the container, run it with docker exec:
docker exec madz node index.js "Summarize memory/_index.md"
All file operations use a virtual filesystem where / is the application root. When you see a path like /package.json or /src/tools/index.js, the leading / is a virtual path that resolves relative to the application's working directory.
The orchestrator uses a CompositeBackend that routes file operations to different backends based on path prefix:
| Virtual Path | Backend | Actual Resolution |
|---|---|---|
/package.json | Core Backend | <cwd>/package.json |
/src/tools/index.js | Core Backend | <cwd>/src/tools/index.js |
/memory/context/profile.md | Context Backend | <cwd>/memory/context/profile.md |
This creates a clean, consistent namespace where the agent always sees / as the root, regardless of where the application is actually running. Path traversal is validated — resolved paths must stay within their backend's rootDir.
Model Context Protocol (MCP) lets you plug external tools and data sources into madz declaratively. Register servers under a root-level mcp key in config.yaml, and madz discovers their tools at startup and registers them alongside the built-in tools.
mcp:
docs:
transport: http
url: https://docs.langchain.com/mcp
local-fs:
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
Transports:
| Transport | Fields | Use for |
|---|---|---|
stdio | command, args, env? | Local, process-spawned servers (e.g., npx-launched). |
http | url | Remote servers over Streamable HTTP (the default). |
sse | url | Older servers that only speak HTTP + SSE. |
Classifying tools: Each server can carry an agents list that routes its tools to specific agent types. When absent, tools default to the orchestrator.
mcp:
local-fs:
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
agents: ["coding", "search"]
agents → tools go to the orchestrator.agents: ["coding", "search"] → tools are delegated to those subagents.agents: ["orchestrator", "coding"] → tools are available to both.The agents classification is a madz-specific extension — it lives in your config, not on the server. The MCP server only provides the tool (name, schema, handler).
Behavior:
local-fs_read_file) to avoid collisions.madz can delegate complex, multi-step work to specialized subagents. Instead of you orchestrating each step manually — checking git status, running lint, editing files, committing — you describe the outcome you want in plain language, and the orchestrator routes the work to the right specialist.
You do not type agent names. You state the task; the orchestrator dispatches.
| Agent | Scope |
|---|---|
code-review | Structured reviews: bugs, security, style, performance |
coding | Code editing, debugging, implementation, git operations |
debug | Error tracing, reproduction, and fix proposals |
documentation | Documentation updates, API docs, changelogs |
performance | Benchmarking, bottleneck identification, optimization |
research | Multi-step research with source tracking |
search | Multi-source search (web, docs, codebase) with synthesis |
security-audit | Security scanning and dependency auditing |
seoAnalyst | Keyword density, meta descriptions, SERP analysis |
testing | Test generation, gap analysis, coverage improvements |
textEditor | Text processing — summarize, rewrite, tone, grammar |
translator | Multi-language translation and language detection |
Commit and push:
Commit and push this branch — we have an open PR.
The coding agent checks git status, stages changed files, crafts a commit message following the project's conventional commit format, and pushes to the remote branch. It will not push without explicit approval — see the notes below.
Run lint, fix, commit, push:
Run lint, fix any errors, then commit and push.
The agent runs the project's lint command, identifies each issue, applies fixes, re-runs lint to verify, and proceeds to commit and push. If a fix is ambiguous or risky, it pauses and asks for guidance rather than guessing.
Update a PR:
Update PR #123 with the latest changes from this branch.
The agent reads the current branch state, diffs against the target branch, and composes a meaningful description from the actual code changes.
Create an issue:
Create an issue for the memory leak in the session manager.
The agent searches the codebase for relevant context, references related files, and creates the issue with a proper title, description, and labels.
Debug a failing test:
Debug the failing test in tests/unit/skills.test.js and fix it.
The agent runs the test, analyzes the failure, traces the root cause through the relevant source files, applies a fix, and re-runs the test to confirm the fix without regressions.
AGENTS.md rules, respects the project's commit message format, and adheres to linting and formatting standards.These guardrails exist so you can delegate with confidence. The agent is a collaborator, not an autonomous actor. It acts, but it does not overreach.
| Issue | Solution |
|---|---|
docker: command not found | Install Docker Desktop (macOS/Windows) or Docker Engine (Linux). Verify with docker --version. |
| Is the container running? | Check status with docker ps -a | grep madz. If it's Exited, check logs with docker logs madz. |
Permission denied on docker commands | Add your user to the docker group: sudo usermod -aG docker $USER, then restart your terminal. |
| Container exits immediately | Check logs: docker logs madz. Missing OPENAI_API_KEY or invalid config will cause early exit. |
| SSH connection refused | Ensure port mapping is correct (-p 2222:22). Try ssh -o StrictHostKeyChecking=no -p 2222 madz@localhost. |
| Memory/skills not persisting | Use volumes or bind mounts for persistent state. If using bind mounts, verify host directory permissions allow the madz user to read and write. |
| Need a shell for inspection | SSH login launches the TUI directly — there is no interactive shell. Use docker exec -it madz /bin/sh instead. |
| Issue | Solution |
|---|---|
| TUI not launching? | Check the configuration — it's always a configuration issue. |
| Skill not executing? | Check that the required permissions (filesystem:read, filesystem:write, etc.) are enabled in config.yaml under sandbox.permissions. |
| Session not persisting? | Verify that memory/ is writable and not mounted as read-only. |
Deploy with confidence. The machine waits for no one, but madz remembers everything.
© 2026 Jason Mulligan. ALL RIGHTS RESERVED.
VIEW SOURCE ↗