# The Oatmilk CLI > Install the oatmilk command line, sign in, use the full-screen app, script Oatmilk and connect agents through it. # The Oatmilk CLI > Oatmilk in a terminal: a full-screen app, Ask AI, scripts, CI and a bridge for AI agents. Source: https://app.getoatmilk.com/docs/terminal The Oatmilk command line, `oatmilk`, opens your workspace in a terminal. It has the same pages, records and actions as the web app, with Ask AI in a message box at the bottom. Scripts, CI jobs and other agents can use it without the full-screen app, and agents that can only start a command can reach Oatmilk's MCP tools through it. It works with the hosted Oatmilk and with [your own installation](https://app.getoatmilk.com/docs/self-hosting.md), and it signs in as you: your role in each company decides what it can see and do. ## Try it in a minute You need Node.js 22 or newer. ```bash title="Start it" npx @getoatmilk/cli ``` ## The first time The first time it starts, the app asks one question: how you want to use Oatmilk. - **Sign in to Oatmilk.** Your account at getoatmilk.com, with nothing to install. If Ollama or LM Studio is running with a model that calls tools, it offers to run Ask AI on it, so your questions stay on your computer while your books stay in Oatmilk. - **Keep everything on this computer.** Your account, books, files and AI models all on your computer, with no sign-up. It needs Docker, [Bun](https://bun.sh) and [Ollama](https://ollama.com) or [LM Studio](https://lmstudio.ai). Setup finds them, picks models that fit, downloads missing ones into Ollama, asks your name and your company's name, starts Oatmilk and signs the terminal in. - **More options.** Choose for each part (sign-in, books, files, Ask AI and the agents, classifiers, reading documents, email and web research, each on your computer, in the cloud or off), use your own cloud services with AI on your computer, or connect to an Oatmilk you or your team already run. Each step shows where you are (`2/5`), and the setup says how local it is in words, like `85% local`. ```bash oatmilk setup # choose again oatmilk setup --local --yes # everything on this computer, without questions oatmilk setup --local --cloud-for agents --yes oatmilk status # where each part runs, the server, the models, Ask AI oatmilk local start # also: stop, restart, logs, open, password, backup, wipe oatmilk models # the models Oatmilk uses; models test, models pull oatmilk ai use ollama # Ask AI in the terminal on a model on this computer ``` `--local` and `--cloud` pick where any command goes, `--offline` refuses an Oatmilk on the internet, `--no-start` leaves a stopped local Oatmilk stopped, and `--no-ai` opens the app without Ask AI. ## Sign in On an Oatmilk that setup made on your computer, the terminal signs itself in. Otherwise the app asks how to sign in: choose the browser, approve, and you land on Home. From a command line, the same steps are: ```bash npm install -g @getoatmilk/cli # or keep using npx oatmilk login # opens your browser; approve, then go back to the terminal oatmilk # the full-screen app ``` On an SSH session or a machine without a browser, run `oatmilk login --device` and paste back the code Oatmilk shows you on any other device. Browser sign-in is the same OAuth flow AI assistants use for [MCP](https://app.getoatmilk.com/docs/mcp.md), and your role in each company still decides what you can do. An [API key](https://app.getoatmilk.com/docs/authentication.md) belongs to one company and keeps exactly the permissions it was given. Sign-ins are saved for your user only, and `oatmilk logout` revokes them. On your own Oatmilk, `oatmilk login --password` signs in with an account's email and password. ## Your data An administrator can take everything out, or close the company for good: ```bash oatmilk export # every record and file, into a folder oatmilk orgs close # offers an export, then asks for the company's name oatmilk wipe # remove every sign-in and setting from this computer oatmilk local backup # the Oatmilk on this computer: its database and files, to restore later ``` The workspace has the same in **Settings › Your data**. ### Moving between your computer and the cloud Books don't move by themselves when you change where Oatmilk runs, and the app says so before you switch. - **To another computer, or your own cloud database and storage:** `oatmilk local backup`, then restore it with the same `self-host/.env` ([Operate](https://app.getoatmilk.com/docs/self-hosting/operate.md#restore)). Nothing is lost. - **Between Oatmilk Cloud and your computer:** `oatmilk export --cloud` or `oatmilk export --local` keeps a full copy of every record and file. Bringing that copy into the other Oatmilk isn't possible yet, so keep using the one that has your books, or start fresh in the new one. Passwords, provider keys and bank or tax numbers are left out of an export, so connections are made again in a new Oatmilk. ## What you can do with it | Use | Start with | Guide | | --- | --- | --- | | Browse and act on the books in a full-screen app | `oatmilk` | [Use the full-screen app](https://app.getoatmilk.com/docs/terminal/app.md) | | Add receipts and invoices from files | `oatmilk add receipt.pdf` | [Add a receipt](https://app.getoatmilk.com/docs/terminal/app.md#add-a-receipt) | | Log hours and send your timesheet, as a contractor | `oatmilk hours add 2h "Design review"` | [Log hours as a contractor](https://app.getoatmilk.com/docs/terminal/hours.md) | | See where your data comes from, and connect more | `oatmilk connect` | [Connect your data](https://app.getoatmilk.com/docs/terminal/app.md#connect-your-data) | | Ask AI on Ollama or LM Studio, with your books in Oatmilk | `oatmilk ai use ollama` | [Ask AI on a model on your computer](https://app.getoatmilk.com/docs/terminal/app.md#ask-ai-on-a-model-on-your-computer) | | Ask AI one question from a script | `oatmilk -p "What needs me today?"` | [Scripts and CI](https://app.getoatmilk.com/docs/terminal/scripts.md) | | Run any API action | `oatmilk api entries.list '{"limit":5}'` | [Scripts and CI](https://app.getoatmilk.com/docs/terminal/scripts.md#run-any-api-action) | | Give an agent Oatmilk's tools | `oatmilk mcp` | [Connect agents through the CLI](https://app.getoatmilk.com/docs/terminal/agents.md) | ## Guides 1. [Install the CLI](https://app.getoatmilk.com/docs/terminal/install.md): npm, other package managers, standalone binaries or from source. 2. [Sign in and choose a company](https://app.getoatmilk.com/docs/terminal/sign-in.md): browser, another device or an API key, against the hosted Oatmilk or your own. 3. [Use the full-screen app](https://app.getoatmilk.com/docs/terminal/app.md): the sidebar, lists, records, the ⌃K palette and Ask AI. 4. [Log hours as a contractor](https://app.getoatmilk.com/docs/terminal/hours.md): log time, send your timesheet and say when you had no hours. 5. [Scripts and CI](https://app.getoatmilk.com/docs/terminal/scripts.md): `exec`, `api`, `actions`, `search`, JSON output and exit codes. 6. [Connect agents through the CLI](https://app.getoatmilk.com/docs/terminal/agents.md): a local MCP server for Claude Code, Codex, Cursor and agents on local models. 7. [Command reference](https://app.getoatmilk.com/docs/terminal/commands.md): every command, option, environment variable and exit code. ## In scripts Set `OATMILK_API_KEY` instead of signing in, and read results as JSON. ```bash title="Ask AI and the API without the app" oatmilk -p "Which receipts am I missing this month?" --output-format json oatmilk exec "Categorize the Uber rides as Travel" --yes oatmilk api entries.list '{"limit":5}' ``` When Ask AI needs an approval or an answer it couldn't get, `exec` exits with code `7`, and the JSON result lists what it is waiting on. Run it again with `--yes` to approve. The [command reference](https://app.getoatmilk.com/docs/terminal/commands.md#exit-codes) lists every exit code. # Install the CLI > Install oatmilk with one command, npm, a standalone binary or from source, and keep it up to date. Source: https://app.getoatmilk.com/docs/terminal/install The CLI is one file with no dependencies to install. Its command is `oatmilk`. ## Before you start - **Node.js 22 or newer**, unless you use a standalone binary. Check with `node --version`. - **A terminal at least 80 columns wide** for the full-screen app. Headless commands work anywhere, including CI. - **An Oatmilk account**, on the hosted Oatmilk or [your own installation](https://app.getoatmilk.com/docs/self-hosting.md). ## Install it with one command On macOS and Linux: ```bash curl -fsSL https://getoatmilk.com/install.sh | sh ``` The script checks for Node.js 22, downloads `oatmilk`, checks it against the SHA-256 that getoatmilk.com publishes beside it, puts it in `~/.oatmilk/bin` and adds that folder to your shell's `PATH`. Open a new terminal and run `oatmilk`. An install made this way [updates itself](#update). | Setting | Does | | --- | --- | | `OATMILK_INSTALL_DIR` | Installs somewhere other than `~/.oatmilk` | | `OATMILK_NO_MODIFY_PATH=1` | Leaves your shell's startup files alone | On Windows, use npm (below). ## Try it without installing `npx` downloads the latest version and runs it. Nothing stays installed apart from npm's cache. ```bash npx @getoatmilk/cli --version npx @getoatmilk/cli ``` Put `npx @getoatmilk/cli` wherever these guides write `oatmilk`. ## Install it with npm ```bash npm install -g @getoatmilk/cli oatmilk --version ``` Other package managers work the same way: ```bash pnpm add -g @getoatmilk/cli yarn global add @getoatmilk/cli bun add -g @getoatmilk/cli ``` > [!TIP] > If your shell says `oatmilk: command not found` after installing, npm's global folder isn't on your `PATH`. `npm prefix -g` prints the folder; add its `bin` folder (on Windows, the folder itself) to `PATH`, then open a new terminal. ## Standalone binaries Each CLI release on GitHub has binaries that don't need Node.js, for people with access to the Oatmilk repository: | Computer | File | | --- | --- | | Mac with Apple silicon | `oatmilk-darwin-arm64` | | Mac with an Intel processor | `oatmilk-darwin-x64` | | Linux on x64 | `oatmilk-linux-x64` | | Linux on ARM | `oatmilk-linux-arm64` | | Windows | `oatmilk-windows-x64.exe` | Download the file for your computer and `SHA256SUMS` from the same release, then check it and put it on your `PATH`: ```bash title="macOS or Linux" sha256sum --check --ignore-missing SHA256SUMS # on macOS: shasum -a 256 --check --ignore-missing SHA256SUMS chmod +x oatmilk-linux-x64 sudo mv oatmilk-linux-x64 /usr/local/bin/oatmilk oatmilk --version ``` On macOS, if the system refuses to open a downloaded binary, allow it in **System Settings › Privacy & Security**, or run `xattr -d com.apple.quarantine /usr/local/bin/oatmilk`. ## From source If you have Oatmilk's source, for example to [self-host it](https://app.getoatmilk.com/docs/self-hosting.md), you can build the CLI from the same checkout. You need [Bun](https://bun.sh) and Node.js 22 or newer. ```bash bun install bun run build:cli # writes packages/cli/dist/oatmilk.js and checks it runs node packages/cli/dist/oatmilk.js --version npm install -g ./packages/cli # optional: makes oatmilk a command ``` Run `bun run build:cli` again after pulling changes. ## Check that it works ```bash oatmilk --version oatmilk --help oatmilk whoami # exits with code 3 and a hint until you sign in ``` Next, [sign in](https://app.getoatmilk.com/docs/terminal/sign-in.md). ## Update ```bash oatmilk update # installs the newest version the way oatmilk was installed oatmilk update --check # only says whether there is one ``` When a new version is out, the app shows it at the top right, like `↑ 0.3.0 ⌃U`. Press `⌃U` (or type `/update`) to install it; restart `oatmilk` to use it. An install made with `install.sh` updates itself in the background instead, and tells you when it has. The app checks at most every 12 hours, never slows its start, and never checks in CI. Choose how it updates, here or under **Settings › Updates** in the app: ```bash oatmilk update auto # update by itself (the default) oatmilk update notify # only tell me oatmilk update off # never check ``` An npm, pnpm, Yarn or Bun install is updated with the same package manager, for example `npm install -g @getoatmilk/cli@latest`. `npx @getoatmilk/cli@latest` always runs the newest version. A standalone binary is updated by downloading the new one. ## Uninstall Sign out first, so the sign-ins it saved are revoked, then remove it and its files: ```bash oatmilk logout --all npm uninstall -g @getoatmilk/cli # or, for install.sh: rm -rf ~/.oatmilk rm -rf ~/.config/oatmilk ~/.cache/oatmilk # macOS also: ~/Library/Caches/oatmilk ``` On Windows, the folders are `%APPDATA%\oatmilk` and `%LOCALAPPDATA%\oatmilk`. The [command reference](https://app.getoatmilk.com/docs/terminal/commands.md#files-it-keeps) lists everything the CLI saves. # Sign in and choose a company > Connect the CLI to the hosted Oatmilk or your own, with your browser, another device or an API key. Source: https://app.getoatmilk.com/docs/terminal/sign-in The CLI signs in the way an AI app does: you approve it in your browser, and it acts as you. Your role in each company still decides what it can see and do. For scripts and CI, an [API key](https://app.getoatmilk.com/docs/authentication.md#api-keys) works instead. ## 1. Choose the Oatmilk to connect to Without `--host`, the CLI talks to the hosted Oatmilk at `https://app.getoatmilk.com`. For any other Oatmilk, pass its address once when you sign in; the CLI remembers it for later commands. | Oatmilk | Address | | --- | --- | | The hosted Oatmilk | Nothing to add | | Your own, on this computer | `--host https://oatmilk.localhost` | | Your own, on a server | `--host https://books.example.com` | | A development server from `bun run dev` | `--host localhost:3000` | The address is read from `--host` first, then the `OATMILK_HOST` environment variable, then the last address you signed in to. Addresses must use `https`; plain `http` is allowed only for `localhost`. ## 2. Sign in with your browser ```bash oatmilk login # or, for your own Oatmilk: oatmilk login --host https://books.example.com ``` 1. Your browser opens Oatmilk's sign-in page. Sign in if you aren't already. 2. Oatmilk shows what the CLI asks for. Choose **Allow**. 3. The page says you're signed in. Go back to the terminal: it shows your email, company and role. The browser sends the sign-in back to a one-time address on your own computer (`http://localhost:/callback`), so nothing to copy. If no browser opens, the CLI prints the address to open; `--no-browser` or `OATMILK_NO_BROWSER=1` always prints it instead. ## Sign in on another device Over SSH, in a container, or on a machine without a browser: ```bash oatmilk login --device ``` 1. The CLI prints an address. Open it on any device: your laptop or your phone. 2. Sign in and choose **Allow**. 3. Oatmilk shows a code with a **Copy code** button. Paste it into the terminal. The code only works for the terminal that asked for it, and only once. ## Sign in with an API key An API key belongs to one company and keeps exactly the permissions it was given, so it suits scripts, CI and shared machines. Create one in **Developers › API keys** in Oatmilk, then: ```bash oatmilk login --api-key # asks for the key without showing it echo "$KEY" | oatmilk login --api-key # or pipe it in ``` For a single command or a CI job, skip `login` and set the key in the environment. `OATMILK_API_KEY` and `OATMILK_TOKEN` (an OAuth access token) win over a saved sign-in. ```bash OATMILK_API_KEY=oat_live_… oatmilk whoami ``` ## 3. Check who you are ```bash oatmilk whoami oatmilk whoami --json ``` It shows your email, the company, your role, the address and how you signed in. With an API key it also lists the key's permissions. ## 4. Choose a company A browser sign-in can open every company you belong to. The CLI works in one at a time, and `oatmilk whoami` shows which. ```bash oatmilk orgs # the companies you can open; ● marks the current one oatmilk orgs use "Contoso" # switch by name or ID oatmilk api entries.list --org org_2abc… # one command in another company ``` `OATMILK_ORG` sets the company for every command. An API key always works in its own company, so `orgs use` asks you to sign in with the browser instead. ## Your own Oatmilk A [self-hosted Oatmilk](https://app.getoatmilk.com/docs/self-hosting.md) signs you in through its own accounts, with the same steps. Two things differ on this computer's `*.localhost` address: - **The certificate.** Oatmilk's local HTTPS certificate comes from its own certificate authority, which Node.js doesn't trust by default. Point `NODE_EXTRA_CA_CERTS` at the file `bun run self-host cert` saves, in every terminal that runs the CLI: ```bash export NODE_EXTRA_CA_CERTS="$HOME/oatmilk/self-host/oatmilk-local-ca.crt" # where your Oatmilk checkout is oatmilk login --host https://oatmilk.localhost ``` - **The name.** `*.localhost` names always mean this computer. The CLI knows that without an `/etc/hosts` entry. A server with a real domain and a Let's Encrypt certificate needs neither. ## Sign out ```bash oatmilk logout # this address oatmilk logout --all # every address you signed in to ``` Signing out revokes the browser sign-in, so the saved token stops working everywhere. To stop an API key, delete it in **Developers › API keys**. ## Where sign-ins are saved Sign-ins live in `credentials.json` in `~/.config/oatmilk` (`$XDG_CONFIG_HOME/oatmilk` when that is set, `%APPDATA%\oatmilk` on Windows). Only your user can read it. `OATMILK_CONFIG_DIR` moves it, for example to keep a separate sign-in per project. Browser sign-ins refresh themselves; run `oatmilk login` again if one stops working. ## When it goes wrong | Message or exit code | What to do | | --- | --- | | Exit code `3`, "You're not signed in" | Run `oatmilk login`, or set `OATMILK_API_KEY`. | | "Oatmilk addresses must use https" | Use `https://…`. Only `localhost` may use `http`. | | "That isn't an Oatmilk API key" | Keys start with `oat_`. Copy the whole key again. | | A certificate error on `*.localhost` | Set `NODE_EXTRA_CA_CERTS` as shown in [Your own Oatmilk](#your-own-oatmilk). | | Exit code `4`, not allowed | Your role, or the key's permissions, don't allow that action. Check with `oatmilk whoami`. | | The browser never comes back | Cancel with Ctrl+C and use `oatmilk login --device`. | # Use the full-screen app > Move around the workspace, act on records and talk to Ask AI without leaving the terminal. Source: https://app.getoatmilk.com/docs/terminal/app Run `oatmilk` to open the app. It shows the same workspace as the browser, for your role, with Ask AI's message box always at the bottom. ```bash oatmilk # opens Home oatmilk "What needs me today?" # opens the app and asks Ask AI straight away oatmilk --page /finance/transactions # opens a page by its address in the web app ``` ## The screen - **Sidebar.** The same pages as the workspace: Home, Inbox, Checklist, Transactions, Reimbursements, Invoices, Reports, Agreements, Contractors, Settings and more, depending on your role. It hides when the terminal is narrower than 80 columns. - **The page.** A list, with the open record beside it: transactions, merchants, trips, statements, invoices, History, webhooks and more. Pages without a terminal view yet, such as Calendar, say what the page is for, suggest questions for Ask AI and link to the browser. - **Ask AI.** The message box at the bottom. Its chat docks on the right when the terminal is at least 120 columns wide. - **The header and status bar.** The company you're in (and the address, when it isn't the hosted Oatmilk), the keys that work right now, and whether Ask AI is working. `Tab` and `⇧Tab` move between the sidebar, the page and the message box. Press `?` for every key. ## Lists and records | Key | Does | | --- | --- | | `↑` `↓`, `j` `k`, `PgUp` `PgDn`, `g` `G` | Move | | `⏎` or `→` | Open the record beside the list | | `Esc` or `←` | Close it, or go back to the sidebar | | `/` | Search this list | | `[` `]` | The previous or next view, such as Needs you or Done | | `i` | Ask AI about the highlighted record | | `o`, `y` | Open it in the browser, copy its link | | `R` | Refresh | | Letters | The record's actions, shown at the bottom: `a` approve, `c` categorize, and so on | Lists load more as you scroll, and every change shows at once. The browser shows the same change, because both use the same API. ## Jump anywhere with ⌃K `⌃K` opens the palette. Type a page name, a merchant, an invoice number or a contractor's name, and press `⏎`. It also runs any API action by name, such as `invoices.void`, starting you off with its required fields. ## Ask AI Type in the message box and press `⏎`. Ask AI is the same as in the browser: the same tools, the page you're on as context, and your chats in both places. - **Approvals.** Before Ask AI deletes, voids, grants access or emails people, the message box asks you. Nothing changes until you approve. - **Questions.** When Ask AI needs an answer, its choices appear in the message box. Choose one with `↑` `↓`. - **Navigation.** "Take me to receipt matching" moves the app there. When Ask AI offers a page, `⌃G` opens it. - **The chat.** `⌃O` shows or hides it, `PgUp` `PgDn` scrolls it, and `Esc` stops an answer. `⌥⏎` or `⌃J` adds a new line. When the app starts, it picks up your latest chat from the last 30 minutes. ## Ask AI on a model on your computer Ask AI can think on [Ollama](https://ollama.com), [LM Studio](https://lmstudio.ai) or any OpenAI-compatible server on your computer, while your books stay where they are, including in Oatmilk Cloud. The model reads them through Oatmilk's tools, signed in as you, so it can do only what you can. The questions, the answers and everything the model reads stay on your computer. ```bash oatmilk ai use ollama # starts Ollama if it's installed, downloads a model if it has none, and checks it oatmilk ai use lmstudio # or a model already loaded in LM Studio oatmilk ai # where Ask AI thinks, and the models found here oatmilk ai use oatmilk # back to Oatmilk's AI ``` In the app, `/model` lists Oatmilk's AI and every chat model it finds, and switches at once. The top bar shows the model while one on your computer is in use. - Pick a model that calls tools, such as Qwen 3.5. `oatmilk ai test` checks it; the list says which models may not. - Every change still asks you first, in the message box, or with `--yes` in scripts. - These chats stay on your computer, so `/chats` and `--chat` are for Oatmilk's AI only. - Background work, such as reading receipts and sorting transactions, keeps running wherever your Oatmilk runs it. To run that on your computer too, [run Oatmilk locally](https://app.getoatmilk.com/docs/self-hosting/your-computer.md). ## Add a receipt **Add a receipt** leads Home, as in the browser, and is in ⌃K. Type the file's path, or drop the file onto the terminal, which types its path for you; several files work too. From a command line: ```bash oatmilk add ~/Downloads/receipt.pdf "~/Desktop/lunch photo.jpg" oatmilk add invoice.pdf --for e0000000-0000-4000-8000-000000000001 # attach it to one transaction ``` Oatmilk reads each file and matches it to its purchase. Adding the same file again does nothing. ## Connect your data **Settings › Connections** lists where the company's data comes from, each with where it stands and one next step on `⏎`. `oatmilk connect` prints the same list. ```bash oatmilk connect # every source and its next step oatmilk connect receipts # the address to forward receipts and invoices to oatmilk connect gmail # opens the page where you connect your inbox oatmilk connect drive # starts Google Drive's sign-in in your browser oatmilk sync # syncs Wise and Stripe now ``` Keys and sign-ins for Stripe, Wise, Notion, Gmail and Google Drive are entered only in Oatmilk in your browser, so they never pass through the terminal. The terminal opens the right page and shows the result. What you see depends on your role: everyone gets their receipts address, inboxes and accounts, finance also gets Wise, and administrators get every source. ## Commands in the message box Type `/` to list them. | Command | Does | | --- | --- | | `/help` | Keys and commands | | `/new` | Start a new chat | | `/chats` | Reopen a recent chat, including one from the browser | | `/model` | Where Ask AI thinks: Oatmilk's AI, or a model on your computer | | `/go` | Jump to a page or record, like ⌃K | | `/search` | Search transactions, email, invoices and more | | `/run` | Run any Oatmilk action | | `/org` | Switch company | | `/theme` | Choose a theme; `/theme matcha` picks one at once | | `/browser` | Open this page in the browser | | `/logout` | Sign out of this machine | | `/quit` | Leave Oatmilk (or press `⌃C` twice) | ## Themes The app has the same themes as Oatmilk in the browser, and fits them to your terminal: on a dark terminal a theme uses its Night colors, on a light one its day colors, and every color is checked to read well on your background. Type `/theme`, or choose **Theme** at the top of **Settings**, and move through the list to see each one on the whole app. `⏎` keeps it on this computer; `Esc` puts back the one you had. ⌃K finds themes by name too. If the colors look wrong for your terminal, set `OATMILK_TERMINAL=light` or `OATMILK_TERMINAL=dark`. ## Tips - Use a terminal at least 120 columns wide to see the list, the record and the chat side by side. - `NO_COLOR=1` turns colours off. - The app needs a sign-in. If you start it signed out, it asks how to sign in first. See [Sign in and choose a company](https://app.getoatmilk.com/docs/terminal/sign-in.md). # Log hours as a contractor > Log time, send your timesheet and check your pay period from the terminal or your AI app. Source: https://app.getoatmilk.com/docs/terminal/hours If a company pays you as a contractor through Oatmilk, you can do your hours without opening the portal. The terminal uses the same timesheet as the portal, so anything you log shows up there too. ## 1. Install it and sign in Accept the invitation email from the company first. Then: ```bash curl -fsSL https://getoatmilk.com/install.sh | sh oatmilk hours ``` The first time, it asks to sign in with your browser. Use the email the invitation went to. On Windows, install with `npm install -g @getoatmilk/cli`. ## 2. Log time ```bash oatmilk hours add 2h "Design review" oatmilk hours add 1h30m "Client call" --date yesterday oatmilk hours add 45m "Fixes" --date mon ``` - **How long:** `2h`, `1h30m`, `90m`, `1.5` or `1:30`, up to 24 hours. - **Which day:** today when you leave it out, or `yesterday`, a weekday like `mon`, or a date like `2026-10-06`. Days still to come aren't allowed. - Logging the same work on the same day twice asks first; `--yes` keeps both. ## 3. Send it for review ```bash oatmilk hours # this pay period: what's logged, what's not sent, when it's due oatmilk hours submit # sends everything not sent yet, after asking oatmilk hours none # tells the company you had no hours this period ``` Entries the company sent back show why, and go out again with the next `oatmilk hours submit`. ## More than one company ```bash oatmilk hours companies oatmilk hours use "Acme" ``` `--org ` picks a company for one command. ## In the app Running `oatmilk` opens your hours. `a` logs time one question at a time, `s` sends the period for review, `[` and `]` move between pay periods, and `o` opens the portal. ## From your AI app Connect Claude, ChatGPT or another AI app to only your own tools, then ask it "Log 2 hours today for the design review": ```bash claude mcp add --transport http oatmilk "https://app.getoatmilk.com/api/mcp?toolset=contractor" ``` In the portal, your account menu has **Log hours faster** with the steps for each app. See [Contractors](https://app.getoatmilk.com/docs/contractors.md). ## What stays in the portal Signing agreements, your payment details, tax numbers and the extra fields some companies ask for on each entry. `oatmilk hours open` opens the portal at your timesheet. # Scripts and CI > Ask AI, run API actions and search from scripts, cron jobs and CI, with JSON output and exit codes. Source: https://app.getoatmilk.com/docs/terminal/scripts Every headless command prints its result to standard output and its progress to standard error, so you can pipe the result into `jq` or a file and still see what happened. None of them opens the full-screen app. ## Sign in for a script On your own machine, `oatmilk login` once is enough. On a server or in CI, use an [API key](https://app.getoatmilk.com/docs/authentication.md#api-keys) from **Developers › API keys**, with only the permissions the job needs: ```bash export OATMILK_API_KEY=oat_live_… export OATMILK_HOST=https://books.example.com # only for your own Oatmilk oatmilk whoami --json ``` ## Ask AI one question `oatmilk -p "…"` and `oatmilk exec "…"` are the same: one Ask AI request, with the answer streamed to standard output and its steps to standard error. ```bash oatmilk -p "Which receipts am I missing this month?" oatmilk exec "Summarize overdue invoices" --output-format json echo "What changed this week?" | oatmilk exec oatmilk exec --continue "And last month?" # your latest chat oatmilk exec --chat 7d0c… "Draft the reminder emails" # a particular chat oatmilk exec --page /finance/transactions "What needs me here?" ``` | `--output-format` | Prints | | --- | --- | | `text` (the default) | The answer, as it arrives | | `json` | One JSON result when the answer is finished | | `stream-json` | One JSON event per line: `chat`, `step`, `text`, `input`, `navigate`, then `result` | The JSON result looks like this: ```json title="oatmilk exec … --output-format json" { "chatId": "7d0c4e1a-…", "status": "done", "response": "Three receipts are missing: …", "steps": [{ "callId": "…", "tool": "…", "label": "…", "status": "done" }], "opened": [] } ``` ### Approvals in scripts Ask AI asks before it deletes, voids, grants access or emails people. In a script nobody can answer, so the command stops: `status` is `needs_input`, `pending` lists what it is waiting on, and the exit code is `7`. ```bash oatmilk exec "Void the duplicate invoice INV-1042" --output-format json > result.json if [ $? -eq 7 ]; then jq '.pending' result.json # what it is waiting on oatmilk exec "Void the duplicate invoice INV-1042" --yes # run it again, approving fi ``` `--yes` approves every step of that request, so use it only for prompts you trust. When Ask AI asked a question instead, answer it in the same chat with `oatmilk exec --chat "…"`. ## Run any API action `oatmilk api` runs any action in the [API reference](https://app.getoatmilk.com/docs/api.md) with your sign-in and prints its JSON `data`. ```bash oatmilk api entries.list '{"limit":5}' oatmilk api entries.list @filters.json # input from a file cat input.json | oatmilk api invoices.create - # input from standard input oatmilk api invoices.void --input id=inv_… --input expectedRevision=2 --input reason="Sent twice" --yes oatmilk api entries.list '{"limit":100}' --compact | jq 'length' ``` - `--input key=value` sets one field on top of the JSON. Values that parse as JSON (numbers, `true`, arrays) are used as such, and `a.b=1` sets a nested field. - Changes get an [idempotency key](https://app.getoatmilk.com/docs/idempotency.md) when the action accepts one, so a retried command doesn't act twice. Pass your own with `--idempotency-key`. - Actions that delete, void, grant access or email people ask first in a terminal, and need `--yes` in a script. - Reads, and changes with an idempotency key, retry up to twice through a brief outage or a rate limit. ## Find an action ```bash oatmilk actions # every action oatmilk actions invoice # actions whose name or summary matches oatmilk actions invoices.void # one action: what it does, its permission and its input oatmilk actions invoice --json ``` The list comes from your Oatmilk's `/api/openapi.json` and is cached for a day. `--refresh` reads it again. ## Search transactions and chats ```bash oatmilk search uber oatmilk search --status needs_you --limit 50 --json oatmilk chats # your recent Ask AI chats, from the terminal and the browser oatmilk chats show 7d0c… # one chat's messages ``` `--status` is one of `needs_you`, `working`, `done` or `review`. ## Exit codes Scripts can branch on the exit code instead of parsing messages. With `--json`, errors are JSON too: `{ "error": { "code", "message", … } }`. | Code | Means | | --- | --- | | 0 | Done | | 1 | Failed | | 2 | Wrong usage | | 3 | Sign-in needed | | 4 | Not allowed | | 5 | Not found | | 6 | Rate limited | | 7 | Ask AI needs your input | | 130 | Cancelled | ## Example: a weekly check in CI Save an API key with the **Read** permission as a masked CI variable named `OATMILK_API_KEY`, then run the CLI on a schedule. In GitLab CI: ```yaml title=".gitlab-ci.yml" missing-receipts: image: node:22 rules: - if: $CI_PIPELINE_SOURCE == "schedule" script: - npx -y @getoatmilk/cli -p "List purchases from the last 7 days that still need a receipt, with merchant and amount." > report.md artifacts: paths: [report.md] ``` Any CI service works the same way: Node.js 22, `OATMILK_API_KEY` from its secret store, and `npx -y @getoatmilk/cli`. ## Example: a nightly export with cron ```bash title="export-entries.sh" #!/usr/bin/env bash set -euo pipefail export OATMILK_API_KEY="$(cat ~/.oatmilk-key)" oatmilk api entries.list '{"limit":200}' > "entries-$(date +%F).json" ``` ```bash crontab -e # 0 2 * * * /home/me/export-entries.sh ``` For a larger export, follow [Export transactions](https://app.getoatmilk.com/docs/tutorials/export-transactions.md), and for changes as they happen, use [webhooks](https://app.getoatmilk.com/docs/webhooks.md) instead of polling. # Connect agents through the CLI > Give Claude Code, Codex, Cursor or an agent on local models Oatmilk's tools with oatmilk mcp. Source: https://app.getoatmilk.com/docs/terminal/agents Oatmilk's [MCP server](https://app.getoatmilk.com/docs/mcp.md) runs inside Oatmilk at `/api/mcp`, and most AI apps connect to it directly. Some agents can only start a local command. For those, `oatmilk mcp` is a small MCP server on standard input and output that forwards every request to your Oatmilk, signed in as the CLI. Use it when: - the agent only supports command (stdio) servers; - you want the agent to use the sign-in you already made with `oatmilk login`, or an API key from the environment, instead of its own; - the agent runs on a machine that can reach your Oatmilk but can't open a browser to sign in. ## 1. Sign in once ```bash oatmilk login # the hosted Oatmilk oatmilk login --host https://books.example.com # or your own ``` For an agent that runs unattended, set `OATMILK_API_KEY` in its environment instead, with only the permissions it needs. A read-only key can't change the books, whatever the agent tries. ## 2. Print the setup for your apps ```bash oatmilk mcp config oatmilk mcp config --host https://books.example.com ``` It prints ready-to-paste setup for Claude Code, Codex, Gemini CLI and Cursor, both for connecting to the server directly and through the CLI. ## 3. Add it to your agent **Claude Code:** ```bash claude mcp add oatmilk -- npx -y @getoatmilk/cli mcp claude mcp add oatmilk -- npx -y @getoatmilk/cli mcp --host https://books.example.com # your own Oatmilk ``` **Any app with an `mcp.json`** (Cursor, Windsurf, Claude Desktop and others): ```json title="mcp.json" { "mcpServers": { "oatmilk": { "command": "npx", "args": ["-y", "@getoatmilk/cli", "mcp", "--host", "https://books.example.com"] } } } ``` Leave out `"--host"` and the address for the hosted Oatmilk. If you installed the CLI, `"command": "oatmilk", "args": ["mcp"]` starts faster. ## Choose which tools it sees Agents work better with fewer tools. `--toolset` limits the bridge to one or more of Oatmilk's [toolsets](https://app.getoatmilk.com/docs/mcp.md#choose-what-the-assistant-sees), separated by commas: ```bash claude mcp add oatmilk-receipts -- npx -y @getoatmilk/cli mcp --toolset inbox,transactions ``` ## Agents on local models The bridge forwards to any Oatmilk, so an agent running on Ollama or LM Studio can use a [self-hosted Oatmilk](https://app.getoatmilk.com/docs/self-hosting.md) without anything leaving your network. Small models call tools less reliably, so use a tool-calling model of 9B parameters or more, and an API key with narrow permissions. [opencode](https://opencode.ai) with Ollama and the bridge, in `opencode.json`: ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", "provider": { "ollama": { "npm": "@ai-sdk/openai-compatible", "name": "Ollama (local)", "options": { "baseURL": "http://localhost:11434/v1" }, "models": { "qwen3.5:9b": { "name": "Qwen 3.5 9B" } } } }, "mcp": { "oatmilk": { "type": "local", "command": ["npx", "-y", "@getoatmilk/cli", "mcp", "--host", "https://oatmilk.localhost"], "environment": { "NODE_EXTRA_CA_CERTS": "/path/to/oatmilk/self-host/oatmilk-local-ca.crt" }, "enabled": true } } } ``` `NODE_EXTRA_CA_CERTS` is only needed for a `*.localhost` installation; see [Sign in and choose a company](https://app.getoatmilk.com/docs/terminal/sign-in.md#your-own-oatmilk). ## Check it Ask the agent "Which Oatmilk company am I in?" It should answer with your company's name. If it can't reach Oatmilk: - Run `oatmilk whoami --host …` with the same address. Exit code `3` means the CLI isn't signed in there. - Add `--debug` to the bridge's arguments to print each request to standard error, which most agents keep in their MCP log. - An agent that starts the bridge from a different user or folder may not see your sign-in. Give it `OATMILK_API_KEY`, or the same `OATMILK_CONFIG_DIR`. # Command reference > Every oatmilk command, option, environment variable, exit code and file. Source: https://app.getoatmilk.com/docs/terminal/commands `oatmilk --help` prints a short version of this page. ## Commands | Command | Does | | --- | --- | | `oatmilk [prompt]` | Opens the full-screen app, and asks Ask AI the prompt if you give one | | `oatmilk -p ""` | Asks Ask AI once without the app; the same as `exec` | | `oatmilk setup` | Chooses how to use Oatmilk: sign in to Oatmilk Cloud, keep everything on this computer, or a mix, saying how local it is (`85% local`); `--local`, `--cloud` or `--host` without questions | | `oatmilk status` | Where each part runs and how local, whether the local Oatmilk answers, the models, where Ask AI thinks, and who's signed in | | `oatmilk local ` | The Oatmilk running on this computer; `backup` saves its database and files (Docker) | | `oatmilk models` | Ollama and LM Studio on this computer and the models Oatmilk uses; `models start`, `models test`, `models pull `, `models use` | | `oatmilk ai` | Where Ask AI thinks in the terminal; `ai use ollama\|lmstudio` runs it on a model on this computer, `ai use oatmilk` goes back, `ai test` checks the model calls tools | | `oatmilk login` | Signs in through your browser | | `oatmilk logout` | Signs out of this machine and revokes the sign-in; a local account's saved password is forgotten too | | `oatmilk whoami` | Who you're signed in as, the company, your role and permissions | | `oatmilk orgs` | The companies you can open; `orgs use ` switches; `orgs close` closes the company for good | | `oatmilk connect` | Where the company's data comes from: receipts by email, Gmail and Outlook, bank and cards, Wise, Stripe, Notion and Google Drive, each with its next step; `connect ` takes it | | `oatmilk sync` | Syncs Wise and Stripe now; `sync wise` or `sync stripe` syncs one | | `oatmilk export` | Downloads every record and file of the company into a folder | | `oatmilk wipe` | Removes every sign-in, setting and history from this computer | | `oatmilk update` | Installs the newest version; `--check` only says whether there is one; `update auto\|notify\|off` chooses whether the app updates by itself | | `oatmilk hours` | A contractor's pay period; `hours add