hands-on · the terminals below type back

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.

Part I — the first ten minutes

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.ps1 skips the machine-memory monitors — everything else works.
  • There is no pip install for 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
  • /exclude removes a message from the model's view only — your transcript keeps it, struck through.
  • Message numbers for /pin//exclude are shown in the transcript gutter (TUI) or via /context.
  • /help also lists your custom commands from ~/.oshell/commands/ (chapter 16 is a cookbook).
  • Switching models with /models keeps 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:7b skips 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 model answers 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 $EDITOR for multiline).
  • --yes exists 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 ID deletes 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.json works 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 off pins the current model; /route on resumes. Per-session.
  • $ARGS may 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

Part II — the deep end

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_length to a number to force it (e.g. 8192 to 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.json is created on demand; add it to .gitignore if 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.json is 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 vision pulls 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 doctor shows 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·tool name 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 browser or 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.

~/.oshell/commands/standup.md
Summarize my recent shell history and git commits into a
three-bullet standup. Keep it dry. Focus on: $ARGS
~/.oshell/commands/commitmsg.md
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
~/.oshell/commands/review.md
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
~/.oshell/commands/eli5.md
Explain $ARGS twice: first like I'm five, then like I'm a staff
engineer. Two paragraphs. No hedging.
~/.oshell/commands/oncall.md
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 / keyWhat it does
Getting in
git clone …/ollama_shell.gitget the source (github.com/sunkencity999/ollama_shell)
./install.sh [all|rag|vision|…]install core + tui + machine memory; extras by name
.\install.ps1Windows install (no machine-memory monitors)
oshell / oshell tuichat REPL / full Textual workspace
oshell doctorhealth-check backend, models, disk, features, Mechanic × Drift
In chat
/help · /models · /toolslist commands, switch models, see capabilities
/daydreamthe model free-associates under a starfield
/pin N · /exclude Nkeep / drop a message from context
/mood rain|snow|aurora|ocean|clearset the ambient weather
/route on|offtoggle auto model routing for the session
/attach file.pngattach an image (vision model answers)
/<name> …your commands from ~/.oshell/commands/<name>.md ($ARGS)
Models
oshell modelstable: name, params, quant, disk
oshell models pull family:sizedownload with live progress; resumes
oshell models rm [--yes] namedelete; --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+Gask 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": 0auto 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.jsoneverything it knows, in files you can read
Memory & knowledge
"remember that…" / "forget…"durable facts → memory.json (always on)
./install.sh ragknowledge 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_EMAILenables jira_search, jira_get_issue
CONFLUENCE_URL · CONFLUENCE_API_TOKEN · CONFLUENCE_EMAILenables confluence_search, confluence_get_page
Computer use
fetch_urlstatic pages, changelogs, docs
browser (opt-in)headless: open / type / click / screenshot
desktop GUI (vision-gated)screenshot → act → screenshot, never blind
TUI keys
Esc · Ctrl+Pmenu · command palette
Ctrl+B · Ctrl+Ycopy last code block · copy last reply
Beyond
oshell finetunelocal QLoRA fine-tuning on Apple Silicon (MLX)