Installation
$ curl -fsSL https://kobune.1024.works/install.sh | shThat picks the archive for your machine, checks it against its published .sha256, installs kobune, kobuned and kobune-studio into ~/.local/bin, and writes shell completions for whichever of bash, zsh and fish you have.
Nothing it does needs root, and it prints the one PATH line you may need at the end — in the syntax of the shell you are actually in, so a fish user is told fish_add_path rather than an export line that fish would reject.
What you are installing
There are no releases. nightly is a build of main, replaced on every merge to it, and it is what the command above fetches.
kobune --version prints the crate version and the commit it came from — 0.1.0 (a1b2c3d). The commit is the half that identifies a build. The number in front of it has never been released and does not move when something changes.
So it is not stable in the sense the word usually carries. A flag can be renamed and a default can change, with nothing to read afterwards but the commit that did it. CHANGELOG.md is there for the moment that stops being true.
None of which means unfinished. Every milestone but Firecracker has landed, and the environments the rest of this page sets up work. What has not happened yet is a version that holds still.
Requirements
| Requirement | Notes |
|---|---|
| A container runtime | Docker, OrbStack or colima — or Apple Container on macOS 26+ |
| macOS | Fully supported. Linux works for the core, minus launchd socket activation |
| Rust 1.88+ | Only to build from source |
The desktop app is optional and needs a little more; see The desktop app.
The install script
Read it before you run it — install.sh is about 700 lines of POSIX shell and does nothing surprising. Three settings:
| Variable | Description |
|---|---|
KOBUNE_INSTALL_DIR | where the binaries go, ~/.local/bin by default |
KOBUNE_CHANNEL | which release tag to fetch, nightly by default — and today the only one there is |
KOBUNE_NO_COMPLETIONS | set to anything to skip the completion scripts |
$ curl -fsSL https://kobune.1024.works/install.sh | KOBUNE_INSTALL_DIR=/usr/local/bin shThe PATH line
If the install directory is not already on PATH, the script says how to add it — for one shell, the one you are in:
fish_add_path ~/.local/binecho 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
. ~/.zshrc# ~/.bash_profile on macOS, ~/.bashrc on Linux: a login shell reads only
# the first, and macOS Terminal opens login shells.
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bash_profile
. ~/.bash_profileecho 'setenv PATH $HOME/.local/bin:$PATH' >> ~/.tcshrc
source ~/.tcshrc# in $nu.config-path
$env.PATH = ($env.PATH | prepend '~/.local/bin')# in ~/.config/elvish/rc.elv
set paths = ['~/.local/bin' $@paths]# in $PROFILE
$env:PATH = "$HOME/.local/bin" + [IO.Path]::PathSeparator + $env:PATHIt works out which one you are in from the process tree, not from $SHELL. $SHELL is the login shell, which is a different thing the moment you start fish from a zsh login — and being handed export PATH in fish is exactly the kind of line that gets pasted into a config file and stays broken for months. ksh, mksh and dash are recognised too, and fall back to ~/.profile.
When it cannot tell, it prints all of them and lets you pick, rather than guessing.
It installs the nightly build — what that means, at the top of this page.
Rerunning it upgrades in place. So does kobune update, without needing the network twice or a shell pipeline.
A prebuilt binary, by hand
The same archives the script downloads:
| Machine | Archive |
|---|---|
| Apple Silicon | kobune-aarch64-apple-darwin.tar.gz |
| Intel Mac | kobune-x86_64-apple-darwin.tar.gz |
| Linux x86_64 | kobune-x86_64-unknown-linux-gnu.tar.gz |
$ gh release download nightly --repo hota1024/kobune \
--pattern 'kobune-aarch64-apple-darwin.tar.gz*'
$ shasum -a 256 -c kobune-aarch64-apple-darwin.tar.gz.sha256
$ tar xzf kobune-aarch64-apple-darwin.tar.gz
$ cd kobune-aarch64-apple-darwinmacOS quarantines unsigned binaries
$ xattr -d com.apple.quarantine kobune kobuned kobune-studioThe install script does this for you. Signing is unresolved, so the other option is to build from source. The desktop app is not shipped at all for the same reason: Gatekeeper stops an unsigned .app outright rather than warning about it.
Build it
Kobune is not on crates.io yet, so building means cloning it.
$ git clone https://github.com/hota1024/kobune
$ cd kobune
$ cargo build --release --workspaceThat produces three binaries in target/release:
kobune— the CLI you usekobuned— the daemon it talks tokobune-studio— the dashboardkobune studioserves to a browser
Put them somewhere on your PATH:
$ cp target/release/kobune target/release/kobuned \
target/release/kobune-studio ~/.local/bin/They ship together and expect to sit side by side: the CLI finds the other two by looking next to itself.
Build the dashboard's page first
kobune-studio has a page compiled into it, and cargo build on a fresh clone cannot build one — so a placeholder stands in that says what to run, and the binary serves that instead of the dashboard.
$ cd apps/studio/web
$ pnpm install
$ pnpm buildThen build as above. Only a source build needs this; the release archives carry the page already. Running pnpm build after a cargo build works too — cargo watches web/dist and picks the page up on the next one.
Shell completions
The install script writes these already. To do it yourself, or for a shell it did not find:
$ kobune completions fish > ~/.config/fish/completions/kobune.fish$ mkdir -p ~/.local/share/zsh/site-functions
$ kobune completions zsh > ~/.local/share/zsh/site-functions/_kobune
$ echo 'fpath=(~/.local/share/zsh/site-functions $fpath)' >> ~/.zshrc$ mkdir -p ~/.local/share/bash-completion/completions
$ kobune completions bash > ~/.local/share/bash-completion/completions/kobunefish loads its file with no further setup. zsh needs the directory on fpath, which is why the extra line is there. bash needs bash-completion 2.x installed.
elvish and powershell are also accepted, since they come free with the generator, but nothing is tested against them.
Moving between worktrees
kobune cd feature/user-auth moves the shell to that workspace's worktree, and it needs one line in your startup file. The install script does not add it: writing to the file your shell reads on every start is not something to do without being asked.
$ echo 'kobune shell-init fish | source' >> ~/.config/fish/config.fish$ echo 'eval "$(kobune shell-init zsh)"' >> ~/.zshrc$ echo 'eval "$(kobune shell-init bash)"' >> ~/.bashrcThat defines one shell function named kobune, which passes everything that is not cd straight through to the command. Without it, kobune cd prints the path it would have moved to — a program cannot change the directory of the shell that started it, so the function is the part that can.
Keeping it up to date
$ kobune update
› installing 9f3c1a2…
╭ update ────────────────────────────────────────────────────────────────╮
│ installed 9f3c1a2 │
│ │
│ › the daemon is still the previous build, so run kobune daemon restart │
╰────────────────────────────────────────────────────────────────────────╯update replaces the installation it is run from — the directory holding the kobune that you invoked, not a configured one — with the current nightly. Both binaries go together, because a CLI and a daemon from different builds would not agree on the protocol between them.
The new files are written beside the old ones and renamed into place. A running executable cannot be written to, but it can be replaced, so an update while the daemon is up leaves it running the old build until it is restarted. Which is what the last line is about, and it is the same line on every machine — what differs is what the restart produces. Starting a daemon asks launchd first, so where launchd has the job the restart ends with launchd's daemon, holding 80 and 443 and running the build that just landed. Where there is no LaunchDaemon there is nothing to ask, and it ends with a daemon started directly, on the fallback ports it was already using.
That line is worked out, not printed regardless: with no daemon running there is nothing to replace, and the panel says only what it installed.
To look without installing:
$ kobune update --check
╭ update ─────────────────────────╮
│ available 9f3c1a2 │
│ running c7282b8 │
│ │
│ › install it with kobune update │
╰─────────────────────────────────╯What a new build leaves to do
The panel above can only speak for the build being replaced. Whether the Skill in a repository matches the new one, and whether the LaunchDaemon is the shape the new one writes, are questions only the build that has landed can answer — so it answers them itself, the first time you run it:
$ kobune url web
https://web.myapp.localhost
› kobune changed to 9f3c1a2 since the last run
› the daemon is not this build, so run kobune daemon restart
› this repository's Skill is not this build's, so run kobune skill install --forceEach line is there because something on this machine says so: a daemon answered and the version it gave was not this build's, .claude/skills/kobune/SKILL.md differs from the one this binary carries, the installed plist was written to an older shape. Nothing is guessed — a repository that never had the Skill is not offered one, and a plist from before this was recorded is left alone rather than called old. The daemon line says not this build rather than the previous one, because a daemon you started by hand from a newer binary lands there too; the answer is the same either way.
Two things to know about the Skill line. It is about the repository you are standing in — the check has no list of your repositories, so another checkout with an older copy is not covered by it. And --force is what the command takes, so a SKILL.md you have edited by hand is replaced along with an outdated one; the difference is visible in git diff either way.
It appears once per build, on the first run that is not --json, and this is also what covers the updates kobune update knows nothing about: rerunning install.sh, a package manager, a build of your own. ~/.kobune/build.json holds the commit that last ran, and is the whole of the state involved. The build is written down only once the lines above have been printed, so a run interrupted before that finds them again.
kobune daemon carries no notice of its own: stop returns as soon as the daemon has been asked, so a check made straight after would report the process you have this second stopped as still running. Nothing is recorded either, and the next command says the lot.
It is stderr, like every other remark of the CLI's own, and never appears under --json — an agent's stream stays one document. kobune update --json carries the same steps as data instead:
{
"status": "installed",
"commit": "9f3c1a2…",
"next": [
{
"command": "kobune daemon restart",
"reason": "the daemon is still the previous build"
}
]
}The automatic check
Once a day, after a command has finished, Kobune asks GitHub what nightly points at and says one line on stderr if it is not what you are running:
a newer build is available (9f3c1a2). Run `kobune update`It runs after the command, never before, so a slow network cannot delay output you are waiting for. It is skipped entirely under --json, so nothing an agent parses ever contains it. Every failure is silent: a check that cannot reach GitHub has nothing to say.
Turn it off with an environment variable:
$ export KOBUNE_NO_UPDATE_CHECK=1The answer is cached in ~/.kobune/update-check.json for 24 hours, and the notice is repeated from that cache in between — a warning shown once a day and never again would just be missed.
kobune --version
The flag carries the same check, and unlike the automatic one it asks every time: --version is a question about the build in front of you, and answering it from a cache up to a day old would be answering a different one. The version line is printed first and the check made after, so nothing you asked for waits on the network:
$ kobune --version
kobune 0.1.0 (c7282b8)
› a newer build is available (9f3c1a2). Install it with kobune updateNothing is added when you are already on the published build — the version line said which build this is, and that is the whole of what was asked. --json and KOBUNE_NO_UPDATE_CHECK skip it exactly as they skip the automatic one, and so does a network that cannot be reached.
A build made from source reports nothing either way. It records the commit it was built from, and with no commit to compare there is no honest answer: "up to date" would be a guess, and "out of date" would push you off a build you made on purpose.
$ kobune --version
kobune 0.1.0 (9f3c1a2)Pick a container runtime
Docker
Nothing to configure. Kobune talks to the Docker API directly and never shells out to the docker CLI, so the CLI does not have to be installed — only the API has to be reachable. Docker Desktop, OrbStack and colima all work.
$ kobune doctor
│ …
│ ✓ container runtime docker 29.4.0
│ …Apple Container
Needs macOS 26 or later and the service running:
$ container system startThen set it in kobune.toml:
[runtime]
default = "apple"There are three differences worth knowing before you choose it — see Runtimes.
Start the daemon
$ kobune daemon start
╭ kobuned ─────────────────────────────╮
│ running │
│ │
│ version 0.1.0 (9b2f09b) │
│ protocol 7 │
│ runtime docker 29.4.0, apple 1.2.1 │
│ uptime 0s │
│ socket ~/.kobune/kobuned.sock │
╰──────────────────────────────────────╯You rarely need to do this by hand; any command starts the daemon if it is not already up. The daemon holds the proxy, DNS and the idle timer, which is why something has to stay resident.
The privileged setup
To reach https://web.myapp.localhost with no port number, three things need root, once:
$ kobune setup
╭ setup ─────────────────────────────────────────────────────────────────╮
│ the URLs need 3 steps, and they need root. │
│ each one is shown before it is run, and nothing runs until you say so. │
│ │
│ 1. let launchd hold 80/443/53 (the daemon itself stays non-root) │
│ 2. point *.localhost at Kobune's DNS │
│ 3. trust the local CA, so HTTPS stops warning │
╰────────────────────────────────────────────────────────────────────────╯
1/3 let launchd hold 80/443/53 (the daemon itself stays non-root)
generated plist: ~/.kobune/dev.kobune.daemon.plist
sudo cp ~/.kobune/dev.kobune.daemon.plist /Library/LaunchDaemons/…
…
run this? [y/N] y
✓ done
2/3 point *.localhost at Kobune's DNS
sudo mkdir -p /etc/resolver && printf 'nameserver 127.0.0.1\n' | sudo tee …
run this? [y/N] n
– skipped
…Nothing runs unasked. Every command is on the screen before the question about it is, so what you agree to is what you have just read, and anything you decline is printed again at the end to run by hand.
Say yes to all of it with kobune setup --yes, or read the commands without being asked about any of them with kobune setup --dry-run.
With no terminal to answer at — an agent, a pipe, --json — the commands are printed and none of them are run. An unattended sudo hangs at the password prompt, and from your side it would look like a silent privilege escalation.
Afterwards:
$ kobune daemon restart # it comes back as launchd's job, holding the real ports
$ kobune doctorSkipping it
You do not have to, and nothing has to be configured to skip it. When 80 and 443 cannot be held, the proxy takes 18080 and 18443 instead and the port goes into the URL:
$ kobune url web
https://web.feat-1.myapp.localhost:18443kobune doctor says so rather than leaving you to notice.
To choose the ports yourself, name them — a port you name is used as given and never fallen back from:
$ export KOBUNE_HTTP_PORT=8080 KOBUNE_HTTPS_PORT=8443 KOBUNE_DNS_PORT=15353
$ kobune daemon startDNS has no fallback, because moving it achieves nothing on its own: the /etc/resolver entry names the port, and writing that needs root either way. That part is macOS, not Kobune. kobune doctor prints the exact command, including the right port.
Already run kobune setup?
Then the proxy does not fall back. launchd holds 80 whether or not its job is running, so a refusal there means the job needs starting — and listening somewhere else would hide that. kobune doctor says which it is.
Check the result
$ kobune doctorEvery line comes with a fix when it is not ✓. If something here is red, sort it out before going further — most confusing behaviour later traces back to it.
Where things live
KOBUNE_HOME, ~/.kobune by default, holds the daemon socket, its state file, logs, the local CA, and any generated tunnel configuration.
A Unix socket path is limited to about 100 bytes, so KOBUNE_HOME cannot be somewhere deep. Kobune checks this at startup and tells you rather than failing with an opaque error.
Taking it off again
$ kobune uninstallIt shows what it found — containers, the daemon's state, the binaries, the completions, and the steps that need root — and asks before removing any of it. --dry-run prints the list and stops; --yes skips the question, and is required where there is no terminal to ask at.
Your worktrees are left where they are. They are listed, so you can see what is being kept, and kobune rm is how one goes.
The full list of what it removes, and how the privileged steps are handled, is in the CLI reference.