Learn oshell — zero to daily driver
Ollama Shell is a local-first, agentic terminal assistant for Ollama. This page is two courses in one: Part I gets you running in ten minutes; Part II is the deep end — configuration, memory, machine telemetry, MCP servers, computer use — everything between "neat demo" and "I open this before my inbox." Finish all sixteen chapters and something nice happens.
In every demo: type the ghosted command (any keys work), press Tab to autocomplete, Enter or ▶ run to run it. The green "try it for real" boxes are homework — check them off as you go. Everything is saved on this device.
Install it, talk to it, feed it your shell. Nine chapters, each with the demo, the why, the edge cases, and homework.
1Install & first run
oshell installs from source — clone the repo and run the installer. On
macOS and Linux, ./install.sh sets up the core, the
TUI, and the machine-memory monitors (Mechanic × Drift);
./install.sh all adds every optional feature —
rag, docs, vision, finetune. On Windows,
.\install.ps1 installs everything except the
machine-memory monitors. Then just say its name: oshell
is the chat REPL, oshell tui the full Textual workspace.
git clone https://github.com/sunkencity999/ollama_shell.git
./install.sh
./install.sh all
.\install.ps1
⚙ why it works this way
The installer builds a private virtualenv, installs the core plus the
extras you asked for, links oshell onto your PATH, and — on
macOS/Linux — registers the Mechanic × Drift telemetry monitors as
user services so your machine starts building baselines from day one
(chapter 13 is where that pays off).
Under the hood, oshell is an agent loop that streams events: the model streams tokens; when it emits a tool call the stream pauses, the tool runs locally, and the result is fed back in. Everything you see in the demos — tool lines, vitals, toasts — is that event stream rendered.
⚑ edge cases & flags
- Ollama must be running first:
ollama serve(or the menu-bar app). - Python 3.10+ required; the installer tells you, politely, if not.
- Extras are installable later, one at a time:
./install.sh rag,./install.sh vision, … or live from the TUI Features menu (chapter 12). - Windows:
.\install.ps1skips the machine-memory monitors — everything else works. - There is no
pip installfor this — it ships from the repo, on purpose: you can read every line of the thing you're about to give a shell to.
Try it for real
2Chat & slash commands
Inside the REPL, anything that starts with / is a
command, not a question. /help lists them,
/models switches models mid-conversation,
/tools shows what the model may touch, and
/daydream lets it free-associate under a starfield —
the same one that closes the front-page demo.
Context is yours to steer: /pin keeps a message
forever, /exclude quietly drops one.
⚙ why it works this way
Slash commands are handled by the client — they are never sent to the model, cost zero tokens, and work even while a reply is streaming.
Tools are capability-gated per model: oshell reads the model card and only offers tool-calling to models that were trained for it. Point it at a plain chat model and you get a plain chat loop — no silent failures, no JSON soup in your transcript.
Pin and exclude exist because context is finite. When the window fills, oshell trims oldest-first — except pinned messages, which ride along until you unpin them.
⚑ edge cases & flags
/excluderemoves a message from the model's view only — your transcript keeps it, struck through.- Message numbers for
/pin//excludeare shown in the transcript gutter (TUI) or via/context. /helpalso lists your custom commands from~/.oshell/commands/(chapter 16 is a cookbook).- Switching models with
/modelskeeps the transcript — the new model inherits the conversation.
Try it for real
3Manage models
Models are files on your disk, so oshell treats them like it:
list, pull, remove. Names read family:size-quant — so
qwen3:8b-q4_K_M is the 8-billion-parameter Qwen 3
at 4-bit quantization. Q4_K_M is the everyday choice (half the size,
barely dumber); Q8_0 keeps more precision for twice the disk and RAM.
In the TUI's model picker the same moves are single keys — p pulls,
d deletes — and each row wears a badge with its disk size.
oshell models
oshell models pull qwen3:8b
oshell models rm old:7b
⚙ why it works this way
Disk is not the whole bill. A 5 GB Q4_K_M model wants roughly
that much RAM for weights plus a KV cache that grows with context
length — a 32k-token conversation can add gigabytes. That's why oshell
sizes num_ctx deliberately instead of maxing it (chapter 10).
Quant cheat-math: Q4_K_M ≈ 4.5 bits/weight, Q8_0 ≈ 8.5. Same family, same size, half the footprint — and for chat and tool use the quality gap is usually smaller than the speed gap.
⚑ edge cases & flags
oshell models rm --yes old:7bskips the confirm — for scripts and the very sure.- Pulls resume if interrupted; layers already on disk aren't re-downloaded.
- The ● marks your default model; change it in config or with
/models. - No tag means
:latest— which is whatever the registry says it is. Pin a real tag for reproducibility.
Try it for real
4Pipe the shell in
The oldest trick in Unix, now with a brain on the other end: whatever you
pipe into oshell ask becomes context for the question.
Logs, diffs, schemas, CSVs — any command's stdout. Long input is
tail-truncated at 12,000 characters, because errors live at the end.
Need a bigger brain for one question? -m overrides
the model for just that ask. And because the model is local, your logs go
exactly nowhere.
cat error.log | oshell ask "why is this failing?"
git diff | oshell ask "write the commit message"
oshell ask -m qwen3:8b "…"
⚙ why it works this way
Why the tail and not the head? Tracebacks, panics, and OOM kills are written last. Keeping the final 12k characters preserves the part that actually explains the failure, and a dim notice tells you when trimming happened, so you're never guessing.
Why doesn't the model "forget the top of the file"? Ollama runs
at a 4k-token context unless the client passes num_ctx. oshell
reads the model card, computes the model's trained maximum, and passes
num_ctx on every request — auto-sized, RAM-aware.
Chapter 10 shows the knob.
⚑ edge cases & flags
- Works with any producer:
journalctl,kubectl logs,dmesg,curl… -m modelanswers one question with another model; your config is untouched.- No stdin?
oshell ask "question"works alone, like a one-shot chat turn. - Compose onward:
git diff | oshell ask "commit msg" | git commit -F -.
Try it for real
5Say what you want done
oshell do is the agent with its hand raised, not on
the trigger: describe the outcome, it proposes the exact shell
command, and nothing runs until you say y. e opens the
command for editing first; n walks away. Every command you confirm
is logged to ~/.oshell/do_history.jsonl — and becomes
a few-shot example, so the proposals get more yours over time. Ask
for something reckless and it refuses.
oshell do "convert every png here to webp"
⚙ why it works this way
The y/e/n gate is the contract: the agent may be clever, but your
machine only runs what you read. Successful commands (and only those)
land in do_history.jsonl with the ask that produced them; on
the next similar ask they're injected as few-shot examples, which is why
the second webp run remembered your quality setting.
Refusals are cheap on purpose — a scoped ask ("delete the webp files I generated today in ./out") gets a proposal; an unscoped one does not.
⚑ edge cases & flags
- e puts the command on an editable line (your
$EDITORfor multiline). --yesexists for scripts. Respect it like a loaded tool: it skips the gate.- n cancels and logs nothing — declined proposals never become examples.
- The plan line tells you why it chose the tool it chose (e.g. cwebp missing, ImageMagick found).
Try it for real
6Sessions
Every conversation autosaves after every turn to
~/.oshell/sessions/*.json — plain files you can read,
grep, and back up. oshell sessions lists them;
--resume reopens the last one mid-thought, or any
other by id — a unique prefix is enough. Resuming also restores the
session's model, so a conversation you had with the big model comes back
with the big model.
oshell sessions
oshell chat --resume
oshell sessions rm ID
⚙ why it works this way
Autosave is per-turn, not per-exit, so a crash, a closed lid, or an
overzealous Ctrl+C costs you nothing.
~/.oshell/last_session.json is just a pointer — that's all
--resume reads before loading the real file.
Sessions store the model name because context means different things to different models — replaying a 32k conversation into a 4k model would silently amputate it. Restoring the model keeps the conversation whole.
⚑ edge cases & flags
- Prefix resume needs a unique prefix; if two ids share it, oshell lists both and asks.
oshell sessions rm IDdeletes the file — the id, title, and transcript go together.- Titles are auto-generated from the first exchange; rename from the TUI Sessions menu.
- The files are yours:
jq .messages ~/.oshell/sessions/b41f.jsonworks exactly like you hope.
Try it for real
7Live in your shell
One eval in your .zshrc and
oshell moves in: ⌃G summons it with whatever is on your command
line as the question — perfect for the flag you can never remember — and
mistyped commands get a helpful hint instead of a shrug.
eval "$(oshell init zsh)"
⚙ why it works this way
⌃G is a zle widget — it reads the line buffer, asks,
and writes the answer back onto your prompt, so the loop never leaves the
shell. The typo hint rides zsh's command_not_found_handler:
it fires only when zsh itself gives up, adds no latency to commands that
exist, and answers from the local model.
⚑ edge cases & flags
- ⌃G on an empty line opens a plain one-shot chat prompt.
- The suggested command lands on your prompt to edit — it never auto-runs.
- zsh today; the init script prints plain shell, so reading it takes one minute:
oshell init zsh | less.
Try it for real
8Machine memory, first contact
oshell knows what's normal for your machine.
oshell doctor health-checks the whole stack — backend,
models, disk, optional features, and the Mechanic × Drift
machine-memory servers. Then, in chat, questions like "is this fan speed
normal?" get answered from your telemetry baselines, not vibes.
This chapter is the handshake; chapter 13 is the deep dive.
oshell doctor
⚙ why it works this way
Mechanic and Drift are local MCP servers that
./install.sh registered as user services. Mechanic samples
runtime metrics on a schedule and builds statistical baselines; Drift
snapshots system state. oshell doctor checks that both are
alive and how fresh their data is — a stale snapshot is a warning, not
an error.
⚑ edge cases & flags
- Fresh install? Baselines need a day or two of samples before "normal" means anything.
- Windows installs skip the monitors — doctor reports them as optional-off, not broken.
- Doctor's exit code is scriptable: 0 healthy, non-zero otherwise. Cron it.
Try it for real
9Make it yours
Drop a markdown file in ~/.oshell/commands/ and it
is a slash command — the filename becomes the name,
$ARGS becomes whatever you type after it, in chat and
the TUI alike. Set the sky with /mood rain, restyle
everything with the theme picker (Esc → Theme, previews live), and
let auto model routing do the thinking about thinking: a fast model
for chat, the big one for hard questions — oshell switches and tells you with
a dim "→ model (reason)" note.
⚙ why it works this way
The commands directory is scanned at startup and watched for changes — save the file, the command exists. No registration, no manifest, no restart.
Routing is configuration, not magic. The block lives in
config.json:
"routing": {
"enabled": true,
"fast_model": "gemma4",
"deep_model": "qwen3:8b",
"vision_model": "llama3.2-vision:11b"
}The router scores each turn (length, code presence, question shape) and picks a lane. Every switch is announced in dim text — you always know who answered.
⚑ edge cases & flags
/route offpins the current model;/route onresumes. Per-session.$ARGSmay appear multiple times in a template; empty args substitute to nothing.- A custom command named like a built-in loses — built-ins win the namespace.
- Moods are cosmetic and honest: they never touch the model or your data, just the sky.
Try it for real
This is where oshell stops being a demo and starts being infrastructure. Configuration layers, two kinds of memory, the whole TUI, telemetry baselines, MCP servers, computer use — and a first week that ends with a diploma. We are going to be almost weird about the detail. That's the point.
10Configuration, layer by layer
Four layers, each one beating the last: defaults <
config.json < config.local.json
< OSHELL_* environment variables. The single most
important line in the file is "context_length": 0 —
zero means auto: oshell reads the model's trained maximum from the
model card, caps it to what your RAM can afford, and passes
num_ctx on every request. Without that, Ollama quietly
runs at 4k tokens and the top of your file falls off — the classic silent
truncation bug, permanently fixed.
⚙ why it works this way
Why layers? config.json is your policy — it can live
in your dotfiles repo. config.local.json is this machine's
exceptions (the laptop routes to a smaller deep model than the workstation).
Env vars are for one-offs and CI: OSHELL_MODEL=… oshell ask …
leaves both files untouched.
Temperature defaults to 0.7 — conversational. Drop toward 0.2 for commit messages and code, raise toward 1.0 for daydream-adjacent work. It's a config key and respected per-request by routing lanes.
Everything lives in ~/.oshell/ — one directory to
read, back up, or nuke. No hidden state anywhere else.
⚑ edge cases & flags
- Set
context_lengthto a number to force it (e.g.8192to keep the KV cache lean on a small machine). - Auto caps at the model's trained max — asking a 8k model for 128k is a lie oshell won't tell Ollama.
config.local.jsonis created on demand; add it to.gitignoreif you version your dotfiles.- Every
OSHELL_*var maps 1:1 to a config key:OSHELL_TEMPERATURE,OSHELL_THEME,OSHELL_CONTEXT_LENGTH…
Try it for real
11Memory & knowledge
Two different organs, often confused. Memory is always on: say
"remember that…" and the model saves a durable fact — you'll see
📝 remembered: … in the transcript — into
~/.oshell/memory.json, plain JSON you can read and
edit. Say "forget X" and it's gone. Knowledge is the opt-in RAG base
(./install.sh rag, chromadb underneath): feed it
documents, and the model gains add_knowledge /
search_knowledge tools to consult them on demand.
The rule of thumb: facts about you go in memory ("I'm in Boulder Creek", "prod cluster is nimbus"); facts in documents go in knowledge (runbooks, specs, the 40-page PDF you refuse to reread).
⚙ why it works this way
Memory is small and rides along in the system prompt every turn — that's
why it's for durable, compact facts, and why the model is stingy about what
it saves. Knowledge is embedded into a local chromadb and retrieved only
when the model calls search_knowledge — big corpora, zero
cost until queried.
Both are files in ~/.oshell/. Neither has a sync service,
a cloud, or an opinion about your privacy. They're yours the way your
shell history is yours.
⚑ edge cases & flags
memory.jsonis hand-editable — fix a typo in a fact with your editor; it's read fresh each session.- Knowledge add accepts files or directories; markdown, text, and PDFs (with the docs extra) are chunked automatically.
- RAG not installed? The knowledge tools simply don't exist — capability gating again, no error spam.
Try it for real
12TUI mastery
The workspace is fully keyboard-driven. Esc opens a numbered menu — models, themes (live preview), features, sessions, moods, doctor — and Ctrl+P is the fuzzy command palette when you'd rather type than count. Attach an image and a vision model reads it. The right-hand Tools panel shows heat — tools the model has used this session glow with a ×N count — and the vitals bar under each reply reads out tokens/sec and the context gauge. You can even install optional features from the Features menu while the app runs.
⚙ why it works this way
The TUI is a Textual app
rendering the same event stream as the REPL — same agent, different glass.
Replies stream as raw text, then commit as rendered markdown ("the ghost,
then the pretty version"). The context gauge is honest because chapter 10's
num_ctx is honest: the percentage is of the real
window, not a guess.
Keys worth binding to your fingers: Ctrl+B copy last code block, Ctrl+Y copy last reply, Esc menu, Ctrl+P palette. Pin/exclude live on the transcript too — hover a message for its gutter actions.
⚑ edge cases & flags
- Moods:
/mood rain · snow · aurora · ocean · clear— and a stormy debugging session leaves rain in the dream sky on its own. - Image attach requires a vision model (
./install.sh visionpulls one); routing sends vision questions there automatically. - Theme choice persists; it's the
"theme"key in config.json wearing a nicer interface. - Copy-transcript lives in the palette: Ctrl+P → "copy transcript".
Try it for real
13Machine memory, deep dive
The pair, properly introduced. Mechanic keeps runtime baselines —
CPU, RAM, fans, temperatures, sampled on a schedule, time-of-day aware — so
"is this normal?" has a statistical answer with a z-score attached.
Drift keeps state snapshots — packages, services, launch agents, open
ports — so "what changed?" has a diff. Together they close a loop no cloud
assistant can: mechanic says whether, drift says what,
run_command fixes, mechanic verifies. Watch the
whole loop run:
⚙ why it works this way
Both are local stdio MCP servers, mounted as native tools —
mechanic_is_this_normal, mechanic_baseline_for,
drift_diff_latest, drift_latest… The model calls
them like any other tool; the data never crosses a network interface,
because there is no network interface involved.
Baselines are per-metric and time-aware: 90% CPU is an anomaly at 2am and a Tuesday afternoon compile is not. Drift snapshots on a schedule and before/after installs, so "since the last snapshot" usually brackets the culprit tightly.
⚑ edge cases & flags
oshell doctorshows baseline age and snapshot freshness — the trust indicators for everything above.- The fix step still goes through
run_command, which is an exec-class tool — visible in the Tools panel, logged in Activity. - Ask open-endedly: "what changed on this box since yesterday?" is a first-class question.
Try it for real
14Any MCP server + integrations
Mechanic and Drift aren't special cases — oshell is an MCP client.
Add any stdio MCP server to the mcp_servers block in
config and its tools mount as native tools, prefixed with the server name,
visible in /tools, gated like everything else.
Work uses Jira and Confluence Server? Set the env vars and four Atlassian
tools appear: jira_search,
jira_get_issue, confluence_search,
confluence_get_page.
⚙ why it works this way
MCP (Model Context Protocol) is a plain contract: a server exposes tools over stdio, a client mounts them. oshell speaks it natively, so the whole MCP ecosystem — GitHub, Postgres, filesystems, your own weekend project — plugs in with four lines of JSON and no oshell release.
"mcp_servers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"]
}
}The Atlassian env vars: JIRA_URL · JIRA_API_KEY ·
JIRA_USER_EMAIL, and CONFLUENCE_URL ·
CONFLUENCE_API_TOKEN · CONFLUENCE_EMAIL. The four
tools read and search — they don't write to your tracker.
⚑ edge cases & flags
- Mounted tools show a
server·toolname in/tools— you always know where a capability came from. - A server that talks to the network makes those tools net-class — they join the privacy banner's count.
- Servers start lazily on first use and are restarted if they crash; a broken server disables its tools, not your chat.
Try it for real
15Computer use
Three rungs on one ladder. fetch_url grabs static
pages — fast, simple, enough for docs and changelogs. The hidden browser
(optional install) drives headless Chromium — open, type, click, screenshot —
for logins, dashboards, and anything JavaScript-shaped. And desktop GUI
tools act on your actual screen — vision-gated, so the model
screenshots, looks, acts, and screenshots again to verify. It never clicks blind.
The safety frame, plainly: tools only run when the model calls them in your conversation; network-capable tools are counted in the privacy banner you see at every launch; browser and GUI are opt-in installs that don't exist until you ask for them.
⚙ why it works this way
Escalation is deliberate: each rung costs more (time, RAM, risk), so the model is prompted to use the cheapest tool that can succeed — and the GUI rung additionally requires a vision-capable model, because acting on pixels you cannot see is how horror stories start. The screenshot→act→screenshot pattern gives every GUI step a before and an after in your Activity log.
⚑ edge cases & flags
- The browser is hidden until installed (
./install.sh browseror the Features menu) — it won't even appear in/tools. - Headless browser state (cookies, sessions) lives under
~/.oshell/too — delete the folder, forget the logins. - GUI tools without a vision model configured simply refuse with a pointer to
./install.sh vision.
Try it for real
16Cookbook & your first week
Five commands worth stealing. Each is a complete file — copy it into
~/.oshell/commands/ under the name shown and the
slash command exists immediately.
Summarize my recent shell history and git commits into a three-bullet standup. Keep it dry. Focus on: $ARGS
Write a conventional-commit message for the diff I give you. One-line subject (max 60 chars), blank line, then a short body explaining why, not what. Scope hint: $ARGS
Review the following code like a kind but unfooled senior engineer. Order findings by risk; flag anything that would page someone at 3am. Target: $ARGS
Explain $ARGS twice: first like I'm five, then like I'm a staff engineer. Two paragraphs. No hedging.
I'm on call and something is wrong with: $ARGS Ask me at most three questions, then give one hypothesis and the single safest next command. Consult machine memory first.
Your first week
Adoption is a habit, not a feature. One small thing a day; by Sunday it's your daily driver. These count toward graduation.
The 7-day plan
Finish all sixteen chapter demos and the sky does something.
✦Cheat sheet
The full surface, one screen, print-friendly. (⌘P makes a decent desk card.)
| Command / key | What it does |
|---|---|
| Getting in | |
| git clone …/ollama_shell.git | get the source (github.com/sunkencity999/ollama_shell) |
| ./install.sh [all|rag|vision|…] | install core + tui + machine memory; extras by name |
| .\install.ps1 | Windows install (no machine-memory monitors) |
| oshell / oshell tui | chat REPL / full Textual workspace |
| oshell doctor | health-check backend, models, disk, features, Mechanic × Drift |
| In chat | |
| /help · /models · /tools | list commands, switch models, see capabilities |
| /daydream | the model free-associates under a starfield |
| /pin N · /exclude N | keep / drop a message from context |
| /mood rain|snow|aurora|ocean|clear | set the ambient weather |
| /route on|off | toggle auto model routing for the session |
| /attach file.png | attach an image (vision model answers) |
| /<name> … | your commands from ~/.oshell/commands/<name>.md ($ARGS) |
| Models | |
| oshell models | table: name, params, quant, disk |
| oshell models pull family:size | download with live progress; resumes |
| oshell models rm [--yes] name | delete; --yes skips the confirm |
| p / d (TUI picker) | pull / delete the highlighted model (badges show disk) |
| From the shell | |
| … | oshell ask [-m model] "q" | stdin becomes context (tail-kept at 12k chars) |
| oshell do [--yes] "outcome" | proposes a command; y run · e edit · n cancel |
| oshell sessions [rm ID] | list / delete saved conversations |
| oshell chat --resume [id|prefix] | reopen last (or any) session; restores its model |
| eval "$(oshell init zsh)" | Ctrl+G widget + command-not-found hint |
| Ctrl+G | ask oshell about the command line you're writing |
| Configuration (~/.oshell/) | |
| defaults < config.json < config.local.json < OSHELL_* | the four layers, weakest to strongest |
| "context_length": 0 | auto num_ctx from the model card — the silent-truncation fix |
| "routing": {…} | fast_model / deep_model / vision_model lanes |
| sessions/ · memory.json · do_history.jsonl · commands/ · last_session.json | everything it knows, in files you can read |
| Memory & knowledge | |
| "remember that…" / "forget…" | durable facts → memory.json (always on) |
| ./install.sh rag | knowledge base: add_knowledge / search_knowledge (chromadb) |
| Machine memory | |
| "is this … normal?" | Mechanic: baseline + z-score answer |
| "what changed since …?" | Drift: snapshot diff (services, ports, packages) |
| Integrations | |
| "mcp_servers": {…} | mount any stdio MCP server as native tools |
| JIRA_URL · JIRA_API_KEY · JIRA_USER_EMAIL | enables jira_search, jira_get_issue |
| CONFLUENCE_URL · CONFLUENCE_API_TOKEN · CONFLUENCE_EMAIL | enables confluence_search, confluence_get_page |
| Computer use | |
| fetch_url | static pages, changelogs, docs |
| browser (opt-in) | headless: open / type / click / screenshot |
| desktop GUI (vision-gated) | screenshot → act → screenshot, never blind |
| TUI keys | |
| Esc · Ctrl+P | menu · command palette |
| Ctrl+B · Ctrl+Y | copy last code block · copy last reply |
| Beyond | |
| oshell finetune | local QLoRA fine-tuning on Apple Silicon (MLX) |