← The Karsa Manual

Using Karsa

Karsa runs as a local daemon (karsa-server) with a web UI at http://karsa.localhost:1688 (Karsa's own name on this machine: its sign-in cookie never goes to another program's port on localhost; Chrome and Firefox find it by themselves, Safari once karsa-enforce-security has run). Everything you do lives in projects, and every project is just plain files on disk — versioned in git, yours to keep, readable without Karsa at all. This page is the lay of the land.

The daemon and $KARSA_HOME

One daemon serves the whole machine. Its state lives under $KARSA_HOME (by default ~/.karsa): the project registry (projects.yaml), machine config (KARSA.md), and this manual (doc/). The daemon is a pure function of its --home flag — there is no hidden environment fallback, so a misconfigured launch fails loudly instead of pointing at the wrong data.

The Karsa System project is a special project whose project_home is $KARSA_HOME. Unlike a normal project it runs uncaged — it can manage, fix, create, and import your other projects, and install the tools Karsa depends on. Its home page is what you see at the root of the UI. It's yours alone (Who reaches what, below).

Projects

A project is a unit of work with its own data, its own agents, and its own cage. A new one has no agent yet: its Home shows a Get started card with three steps, each ticked when it's really done (install OpenCode on Mango Inference; link its Mango Inference key, one you have or a new one; pick a model, or a line's newest such as GLM's, and turn it on), then Chat. When another project already has one, Set up like … does all three in one tap, with a new agent of this project's own. The projects list marks a project with no agent yet. You create one by picking a template, or by describing the system you want and letting Karsa scaffold it — collections, automations, schedules — for your use case.

On disk a project lives under {project_home}/karsa/proj_*/:

Every record is versioned in git per project. Run history (each fn run's exec and its log) stays on disk beside it but isn't versioned: it's runtime state, not the record. If you lose the registry, a project can be re-imported from its folder alone, because the folder is self-describing.

Removing, resetting, deleting and importing a project. When a project leaves (Remove keeps its folder, Reset deletes its Karsa records, Delete deletes the folder), its runs stop, its agents' and apps' OS users are retired and the files they made become yours, their keys are deleted, and its folder is closed to every agent. When one arrives (Import), its agents and apps get OS users and their folders again, as if made here, but no keys come with a folder: which key each app and agent uses is cleared, and you pick again. A folder whose agents are already in another project (a copy of it) is refused.

Who reaches what

Your server is yours, the way an iPhone has one owner: you open Settings, and each app keeps to its own sandbox.

Getting around

Collections

Collections are your typed data. Pick the shape that fits:

Each markdown resource has YAML frontmatter (its typed fields) and a markdown body. The UI renders both with the shared frontmatter + markdown views; ids cross-link automatically.

Each collection's list has a filter bar: tap ⚲ Filter to filter on any of the collection's fields (a status, a time range, a name, …), then sort and limit; each filter is a pill you tap to change or ✕ to remove. </> shows the same filter as a short query you can type (status = 'failed' and started_at > -1d order by started_at desc). The filter lives in the page's address, so a filtered list can be bookmarked or shared. Execs and each fn's Run History have the same bar.

Chat — talk to your agents

Open Chat in a project and describe what you want. Your agents answer: each is an AI that works for you in this project, on an agent harness (OpenCode, open source; Claude Code; or Codex), on the model provider you pick: OpenCode on Mango Inference and Claude Code on Mango Inference include their harness and set it up for Mango. Installing one from Apps makes a starter agent, @opencode-mango (say), and makes it the default. Agents (doc_agents) walks through it all.

Files in a chat. 📎 uploads a file and puts its id in your message; any file a message mentions shows under it, an image as a thumbnail, a video with ▶, anything else as a chip. Tap one to see it full screen (‹ › between a message's files). An agent shows you a file the same way: it uploads it to Files and puts the id in its reply.

Your inbox — what nudges you

You › Inbox (the row at the bottom of the sidebar) lists what nudged you, unread first, each with why (reply, mention, keyword: invoice, waits on you). A dot on the menu button and on You says there's something new. Opening a notification takes you to its chat or task, and opening that page settles it; Mark all read settles the rest. Show just the unread ones, your mentions, your keywords, or what waits on you with the chips at the top.

Set one for everything, one per project, and one per chat or task. The most specific wins. Muted silences even a mention.

Apps ask before they use something

An app works inside its own folder freely. Anything beyond that (your project's records, the internet, saving a file to your device) it asks for, like an iPhone app: the first time it needs something, Karsa shows a sheet with what the app wants and the app's reason, and you tap Allow or Don't allow. Karsa remembers your answer. Printing (or saving as a PDF) is asked when you open the app, since it changes how the app's page is loaded. Every app has a Permission page listing what it asks for, with a switch for each, so you can change your mind any time.

Schedule, Browse, Search

Reaching Karsa from away — link the server to your Karsa Cloud account

Karsa answers two ways, and only two: on this machine (karsa.localhost:1688; programs, the CLI and agents use localhost:1688, with a token or the owner socket), and from anywhere through Karsa Cloud (its own https://…host.karsa.dev address). Not over your home network: another device on your Wi-Fi uses the Karsa Cloud address too, which is https. To get that address, the server joins your Karsa Cloud account.

Joining is interactive — the same shape as gh auth login or signing in to a CLI tool. You never type a credential into the server:

ABCD-1234. On the server, run karsa cloud link (or press Link to Karsa Cloud in the Remote app's page). The daemon proves it is itself using a key that never leaves the machine. cloud.karsa.app/link. It prints a short code and a URL — e.g. ABCD-1234 at cloud.karsa.app/link — and waits (the daemon finishes the join even if you close the terminal; karsa cloud status shows it). karsa cloud status. Open that URL on any device, sign in with your email and the undefined-digit code, then enter the short code. undefined. You are shown which server is asking — its id, name, and when it asked. Approve it. undefined. The server picks up the approval and is now linked; the console lists it, and the server shows the account it belongs to.

The approval step is deliberately on the console side, where you can see what you are admitting: a linked account has owner-level access to that server — every project, remotely. A server is linked to one account at a time: while it's linked, no other account can link it. Only you can unlink it, from either side: the console, signed in as the account it's linked to, or karsa cloud unlink --agree_relink_here_within_30_days on the machine, which tells cloud first so your remote access really stops. Unlinking ends the shares of its projects, and asks you to agree that for undefined days it can be linked again to the same account only (karsa cloud link on the machine). It stays in that account's list as Unlinked for those undefined days, counting toward the account's servers. Then it leaves the list, and linking it again, to any account, takes an identity reset (below). A lost account can never strand a server: on the machine, the karsa CLI is you (karsa cloud unlink --local_only works with cloud gone). See the next section for signing in.

Unlinking, and moving a server to another account

A server belongs to the account it first linked to. Karsa Cloud knows it by the key it first asked to link with, which comes from a secret in this machine's keychain, so only this machine can link it again, and only to that account, within undefined days of an unlink. Someone who gets into your Karsa Cloud account can open your server as you until you sign them out (Sign out everywhere in the console), but can't take it to their account.

Moving a server to another account, or linking it again more than undefined days after an unlink, takes a new identity: unlink it, then on the machine run karsa identity reset. It becomes a new server to Karsa Cloud, with a new id, a new key and a new address, and no shares; your projects and secrets stay (re-encrypted with the new key). Only the person who installed Karsa can run it, at the machine: there's no way to do it remotely. Local browser sessions end (karsa open --local signs you in again). It can't be undone.

If the keychain loses the key (say it was reset), Karsa won't start, rather than quietly making a new key: it says to restore the keychain, or to run karsa identity reset. Then secrets encrypted with the lost key can't be recovered: the reset lists them, to set again, and you unlink the old server in the console.

Karsa Cloud emails you about every sign-in to your account, every unlink of one of your servers, and every share you make, each saying what to do if it wasn't you.

Give the server a name and a note for the console with karsa cloud update --display_name "Studio Mac" --description "the desk machine" (or Edit in the console). Cloud also slows down guessing everywhere it matters. Asking for sign-in codes again and again waits a little longer each time (undefined, undefined, then undefined minutes), never longer than a code lasts, so there's always a code that works in your inbox: use the latest one. Three wrong tries and that code stops working on your network (ask for a new one); join codes back off too.

Signing in

Every request to Karsa needs a token, from this machine too. Who you are comes from your OS user, not a password:

Who the karsa CLI is

The CLI has no login. Like an iPhone's Settings, which only you open while each app keeps to its own sandbox, Karsa tells who's calling from how the call arrives:

| Who runs karsa | How it reaches Karsa | Who Karsa takes it for | |---|---|---| | You, the OS user who runs Karsa | the owner socket, $KARSA_HOME/.run/karsa.sock, in a folder only you can open | the owner: everything, owner-only steps included | | An agent, during its run | its run's token ($KARSA_API_TOKEN, also inside $KARSA_API_BASE) | that agent: what it may do, never more | | An agent's or app's OS user outside a run, another account on this Mac, any other program | nothing: it can't open the socket's folder (your home and Karsa's are yours only), and a call without a token is refused (401) | no one |

What tokens don't stop: anything running as you. They keep out other users of this machine, other websites open in your browser, your network, and strangers on the tunnel. A program running as you can still read what you can, Karsa's keychain items included. That's why your agents don't run as you.

Notifications on your phone

Karsa can buzz your phone: an agent replied in one of your chats, a task needs you, an app asks permission. A tap opens that chat or task in Karsa.

Your Inbox and the banner in Karsa always get what your rules allow.

Agents' own OS users

Every agent, and every app running on its own (its warm fns, its service), runs as a hidden OS user of its own, _karsa_<its id>: no login, no password, not in the login window. It can't read your private files, your logins, your keychain or another agent's folder; it reads its project, and an agent writes it. You stay in control of everything they make.

You run one script, once, with sudo. Making OS users takes root, and Karsa never asks for your password. The installer puts karsa-enforce-security beside karsa (in ~/.local/bin): a short shell script that holds every root step Karsa has, and nothing else. Read it, then run it from your own account:

less ~/.local/bin/karsa-enforce-security
sudo ~/.local/bin/karsa-enforce-security

Use its full path: on Linux, sudo doesn't look in ~/.local/bin. It installs two root-owned files, adds Karsa's address to /etc/hosts, and does nothing else:

You run it again only when a Karsa release changes it, not for each app or agent: what any app runs uses the same two files. The update banner then gives you the exact line, that release's own script (sudo sh ~/.karsa/packages/.staging/<version>/karsa-enforce-security), and Karsa knows when it's done by hashing the installed helper: no button to press (see Upgrading).

It refuses to install if anyone but root could change a folder above that script, because whoever could change it could become root; it says how to fix that. karsa-enforce-security --show prints both files first. --check (no root) says whether they're installed, current and working. sudo ~/.local/bin/karsa-enforce-security --remove deletes every _karsa_* user and both files. Add -v to any of them to see each step and each command before it runs.

karsa start checks it every time, and won't start while it's missing or from another version of Karsa (a downloaded next release's counts too, so a restart between its sudo step and Install still starts): it says why and prints the two lines above. From then on Karsa makes the users as agents and apps come, and retires them as they go. A macOS update can change them (undefined.undefined.karsa status set their group back to staff): Karsa puts each back as it made it at its next start. Retired users are deleted when you run the script again: macOS asks before a user is deleted ("Terminal would like to administer your computer"), so Karsa leaves that to you, at the Terminal; click OK. There's no command that clicks it for you (macOS keeps that answer for the person). Over SSH it doesn't ask when Remote Login allows full disk access for remote users (System Settings → General → Sharing → Remote Login ⓘ): run it from your phone's SSH app and nothing pops up at the Mac. karsa status says how many are waiting. If the files go away while Karsa runs, agents and apps stop running (never as you instead), and karsa status, karsa doctor, Settings, the You menu and each refused turn say so.

Your home and Karsa's home become yours only (undefined). On a Mac your home is open to other OS users by default (undefined, so other accounts can reach your Public folder), and many tools leave files anyone can read: ~/.aws/credentials and ~/.docker/config.json often are. Agents still pass through your home to reach a project in it, so Karsa also closes everything else in the folders they pass through to them (an ACL entry naming Karsa's agents only; it changes nothing for you or anyone else), and whatever you add there later. The programs on your PATH in your home (~/.deno/bin, ~/.opencode/bin, a Homebrew installed there with everything it needs) stay reachable, so a harness still finds them; other toolchains in your home that aren't on your PATH aren't the agent's. Karsa checks this at every start. If you share your home on purpose (your Public folder with another account on this Mac, a web server serving ~/Sites), it can't stay open yet.

Karsa itself is the OS user who first started it on this home; it won't start as anyone else (set it up as root, and root it stays).

What each agent can reach

An agent always reads and writes its project and its own folder. Beyond them, each agent has a file-access list: the paths on this machine it may read, or write. Think of an iPhone: the agent's OS user is its sandbox, its list is Settings → Privacy, and the card it shows you is the "Allow access?" prompt. You change the list on the agent's page (File access) or with karsa file-access. A change applies at once, and Karsa applies the whole list again at every start, so the list is what's real.

Two presets. A preset is only the starting list:

Either way, writing outside its project always asks. Set the preset new agents start with in Settings (New agents read). Switch one agent's on its page: the preset's entries change, and yours and its answers stay.

When it's blocked, it asks you. For example, under Manual: "@analyst wants to read ~/Downloads/qundefined-board.pdf: to summarise it." A card shows on whatever Karsa page you have open, and a line goes in its chat. You answer:

Your answer is posted in its chat, which wakes it to carry on.

Hidden files stay closed. In a folder an agent reads, every file or folder whose name starts with a dot (.env, .git) is closed to agents. Karsa does this when you grant the folder and at every start; one made in between stays readable until the next start. Hidden folders aren't the only place secrets live (on a Mac, ~/Library holds browser cookies and app tokens), which is why the presets list folders to open rather than your home minus what's hidden.

Some paths are never asked for from a chat: your keys (~/.ssh, ~/.aws, ~/.gnupg) and Karsa's own home everywhere. On a Mac, all of ~/Library (browser cookies, Mail, Messages, app logins) except your cloud drives in it (iCloud Drive, ~/Library/CloudStorage). On Linux, ~/.config, ~/.local/share and ~/.mozilla. An agent's ask for one is refused. You can still add one yourself on its page, after a warning: anything an agent reads can tell it to ask, and one quick tap would hand over your keys.

You can set it up by asking an agent. Tell it in a chat, for example "give yourself my Code folder and my Dropbox". It can see the names of the folders at the top of your home and your cloud drives, but not what's in them. It asks for all of them in one card: tick the ones you mean, then Always or Once. It can't grant itself anything.

Each entry shows what the machine really allows. Karsa opens the path as the agent's OS user:

karsa file-access list --agent analyst
karsa file-access create --agent analyst --path ~/Pictures/receipts --access read
karsa file-access delete <id>
karsa file-access preset --agent analyst --preset manual
karsa file-access default --preset manual        # what new agents start with
karsa file-access places --agent analyst         # what's there to name: folders and cloud drives, by name
karsa file-access asks                           # the cards waiting for you
karsa file-access answer --request <fask_…> --answer always --scope folder

Commands aren't limited by name. Any interpreter (python3 -c, sh) runs anything, and an agent can write a program into its project and run it, so a list of allowed commands would be a wall with a door. What a command can do is limited instead: what it can read and write (this list), which keys it holds (a secret you passed it: without a GitHub token, git push fails whatever the command), and, later, its network.

Webhooks — ingress URLs that feed a project

Once the server is linked, it can have ingress URLs: public webhook addresses that services like GitHub, Stripe or Slack post to. Each delivery becomes a karsa/ext/ingress event on one project's bus, where a subscriber picks it up.

karsa ingress create --display_name GitHub --project <id|slug>   # prints the URL once
karsa ingress sync                                     # fresh from cloud: name, URL (secret hidden), project
karsa ingress update <id> --project <id|slug>          # where it lands (--project null: the System project)
karsa ingress rotate <id>                              # new secret; the old URL stops within seconds
karsa ingress delete <id>

There is exactly one way to join a server; a paste-a-token variant for headless machines is a possible future addition when servers run as fleets, not something to hunt for today.

Sharing a project

Share a project the way you share a photo album on an iPhone: the other person sees what you picked, in their own Photos, and nothing else of your phone. Here they use what you share from their own Karsa: their chats and agents work with it there, and every call runs on your server, as them, within what you gave. Nobody works inside your server, and the System project is never shared. Both servers need to be linked to Karsa Cloud.

Sharing (the project's Sharing page, Share…): an email, an optional note, and per collection what they may do. Only the project's own collections can be shared (its template's, its apps', yours), never Karsa's (chats, tasks, files, specs), and never an op that runs your code (a collection's api-filter). A new share is read: list, get, and the collection's events. Read & write adds create, update and delete. Karsa Cloud emails them its link, and it's theirs at once: there's nothing to accept. The Sharing page says when they first use it.

Who it is, here. Each share is a person in the project (its Users), named by their email. Whatever they send, by hand or through one of their agents, arrives as that person: you see who wrote a record (_created_by), not which of their agents did. Their writes wake your subscribers like anyone's, so what they write is data for your agents, never instructions (like a webhook's body).

Ending it is yours: End on the Sharing page. Their next call is refused, and their mounts say the share ended. They can only Dismiss it from their list in Karsa Cloud (with a few seconds to undo): it keeps working for them, and you aren't told.

When something is shared with you: the email's link opens Karsa Cloud (cloud.karsa.app/shared/…, also under Shared with you, with who shared it, when, and their note; Dismiss takes one off that list, and its link and your mounts still work). It asks which of your servers to mount it on, your default selected (the first you linked; Make default on a server's page changes it); with one server it goes straight there. On your server, its page lists your projects, the ones that already mount it first: tap one to open it, another to mount it there, or New project… for a blank one.

Mounting (the mount page; also a project's Sharing › Mount…): pick the source (what's shared with you, or another of your projects, from one list), then which of its collections, each with its name here (the source's, or one you type). Each becomes a collection here, listed like your own, whose only file is its index.md: nothing is copied, and every call runs at its source.

Updates

Karsa keeps itself up to date. The Updater, an app in the System project, checks the signed release list every day at --download:download your time, downloads a new release, and installs it by itself (or, in mode download, waits for you to tap Install); each project's apps update with it. A banner on every page says where it is, karsa status says it in one line, and karsa upgrade checks, --downloads, --installs and --undoes by hand. Installing restarts Karsa: running agent turns carry over, open browser tabs too unless the Browser app updates with it, and if the new version doesn't come up, the previous one is put back. To change when it checks, or to stop it installing by itself, ask your System agent, or read Upgrading.

Your projects' built-in parts follow along (spec_undefinedMundefinedYARFKZAD). What a project's template made when the project was made (its collections' descriptors, the task queue's fns and subscribers, the repeating tasks' maker) is updated when Karsa starts after an update, so an old project gets what a new one would. Your changes stay:

A part the template no longer has goes, unless you changed it. Each project keeps what was stamped in karsa/proj_*/.template/ (the base for the next merge), and the project's history gets a "template update: …" commit. To run it by hand, or see what it would do: karsa project template-update --id <project> --dry_run true (the owner only).

Where to go next

When a task produces data, read Data and storage — it's the rule for where each shape of output lives (assets, sqldb + collections, docdb docs) and the rpc API conventions for putting it there. When the task shifts from "organize data" to "build an app," read Building a Karsa app — the app belongs inside the project, on the Karsa SDK, not in a scratch directory wired to an outside service. If you find yourself reaching outside, read Anti-patterns first.