# Koshi 0.6.0 — koshi.kdl (/docs/v0.6/configuration/koshi-kdl)



`koshi.kdl` holds the app settings. It sits directly in the
[config directory](/docs/v0.6/configuration#where-the-files-go). `version` is
required; everything else is optional. `theme`, `image-support`,
`reduced-motion`, `stay-in-pane-placement-mode-after-placement`,
`allow-beta-features`, `allow-other-users`, `remote-listen`, `remote-reconnect`,
`remote-link-timeout-ms`, `max-frames-per-second`, `shared-sessions-dir`, and
`auto-close-session` are top-level; the rest are
grouped into blocks.

<Callout>
  A bad field is skipped at startup, keeps its default, and is logged with its
  line and column — the rest of the file still applies. The exception is `update`: a bad value there drops
  the whole file for that launch. `koshi config check` rejects bad fields
  outright.
</Callout>

Each section says who reads it: the **session**, which took its copy when it
started, or your **terminal**, which reads its own file every time it attaches.
See [Whose setting is it](/docs/v0.6/configuration#whose-setting-is-it).

## theme [#theme]

**Read by your terminal.** Two terminals showing one session can wear different
colours.

`theme "midnight"` loads `themes/midnight.kdl`. A missing, invalid, or omitted
theme — or `"default"` — uses the built-in colours. See
[Themes](/docs/v0.6/configuration/themes).

| Key     | Value                                                   | Default     | Since   |
| ------- | ------------------------------------------------------- | ----------- | ------- |
| `theme` | string — the `themes/<name>.kdl` to use, without `.kdl` | `"default"` | ≥ 0.1.0 |

## pane [#pane]

**Read by the session.** Panes are the session's, so every terminal sees the
same sizes.

| Key        | Value                                                                                                                 | Default | Since   |
| ---------- | --------------------------------------------------------------------------------------------------------------------- | ------- | ------- |
| `min-cols` | integer — smallest width a pane may shrink to                                                                         | `2`     | ≥ 0.1.0 |
| `min-rows` | integer — smallest height a pane may shrink to                                                                        | `1`     | ≥ 0.1.0 |
| `gap`      | integer — blank cells between two panes that meet along a horizontal or vertical split; stacked panes stay contiguous | `0`     | ≥ 0.4.0 |

## scrollback [#scrollback]

**Read by the session**, including `scroll-on-input`, so every attached
terminal gets the same answer.

| Key               | Value                                                                        | Default             | Since   |
| ----------------- | ---------------------------------------------------------------------------- | ------------------- | ------- |
| `max-lines`       | integer — lines of history kept per pane (negative means `0`: no scrollback) | `10000`             | ≥ 0.1.0 |
| `max-bytes`       | integer — byte ceiling on that history (negative means `0`)                  | `33554432` (32 MiB) | ≥ 0.1.0 |
| `scroll-on-input` | boolean — typing while scrolled up snaps the view back to the newest line    | `#true`             | ≥ 0.1.0 |

`scroll-on-input #false` keeps the view parked while the input still goes
through. Only the primary screen follows; the alternate screen is left to the
full-screen program on it.

## layout [#layout]

**Read by your terminal.** Two terminals viewing one session can differ.

| Key                  | Value                                                                                              | Default   | Since   |
| -------------------- | -------------------------------------------------------------------------------------------------- | --------- | ------- |
| `new-pane-direction` | `"left"` \| `"right"` \| `"up"` \| `"down"` — where a new pane opens when the command does not say | `"right"` | ≥ 0.1.0 |

The `new-pane-<side>` keybindings and an explicit `--direction` name their own
side and ignore this.

## mouse [#mouse]

**Read by your terminal** — the mouse is its own.

| Key             | Value                                     | Default               | Since   |
| --------------- | ----------------------------------------- | --------------------- | ------- |
| `border-resize` | boolean — drag a pane border to resize it | `#true`               | ≥ 0.1.0 |
| `scroll-lines`  | integer — lines per wheel notch           | `3`                   | ≥ 0.1.0 |
| `wheel`         | `"scroll-scrollback"` \| `"ignore"`       | `"scroll-scrollback"` | ≥ 0.1.0 |

Dragging the corner of a tiled pane moves both borders that meet there. A
floating pane takes the mouse on every cell it covers; see
[Floating panes](/docs/v0.6/using/floating-panes#mouse).

## copy [#copy]

**Read by your terminal** — the copy is made where the selection was dragged.

| Key                        | Value                                            | Default | Since   |
| -------------------------- | ------------------------------------------------ | ------- | ------- |
| `trim-trailing-whitespace` | boolean — drop trailing blanks from copied lines | `#true` | ≥ 0.1.0 |

## notifications [#notifications]

**Read by your terminal.** Sets the toast that shows each new notification for
a short time. The [notification center](/docs/v0.6/using/notifications) keeps
every notification whatever these keys say.

| Key                 | Value                                                                                                                | Default       | Since   |
| ------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------- | ------- |
| `toast`             | boolean — show a toast for each new notification                                                                     | `#true`       | ≥ 0.6.0 |
| `toast-position`    | `"top-right"` \| `"top-left"` \| `"bottom-right"` \| `"bottom-left"` — the corner of the pane area the toast sits in | `"top-right"` | ≥ 0.6.0 |
| `toast-duration-ms` | integer, `1000` to `60000` — how long a toast stays in place                                                         | `3000`        | ≥ 0.6.0 |

A toast slides in from the side of its corner in 1 second, stays for
`toast-duration-ms`, then slides out in 1 second. When more notifications wait,
each part takes half a second. A value the key does not accept is skipped with
a warning.

## pane-clipboard-writes [#pane-clipboard-writes]

**Read by the session**, when it starts and again when it restarts into a new
build.

A program in a pane can copy text by writing `OSC 52`, such as
`printf '\033]52;c;aGk=\a'`, which copies `hi`, or by running a clipboard tool:
`pbcopy`, `xclip`, `xsel`, or `wl-copy`. This block decides whether koshi
passes those copies on to your terminal.

| Key              | Value                                                                         | Default           | Since   |
| ---------------- | ----------------------------------------------------------------------------- | ----------------- | ------- |
| `allow`          | boolean — pass clipboard copies made by programs in panes on to your terminal | `#false`          | ≥ 0.6.0 |
| `max-copy-bytes` | integer, at least `1` — the longest copy, in decoded bytes                    | `8388608` (8 MiB) | ≥ 0.6.0 |

With `allow #false`, an `OSC 52` copy reaches no clipboard, and a clipboard
tool runs as it does outside koshi.

With `allow #true`:

* An `OSC 52` copy goes to the terminal of every client whose focused pane
  wrote it, and that terminal sets its own clipboard. The copy keeps the
  clipboard it names (`c`, `p`, `q`, `s`, or `0` to `7`). A copy longer than
  `max-copy-bytes` reaches no terminal. A request to read the clipboard gets no
  answer.
* On Linux and macOS, each pane started while it is on finds stand-ins for
  `pbcopy`, `xclip`, `xsel`, and `wl-copy` first on its `PATH`, one for each of
  those tools the session found when it started. A plain-text copy made through
  a stand-in goes to every client attached with `koshi attach --remote` whose
  focused pane is that pane. The real tool runs too, unless every client
  focusing the pane is on another machine and took the copy.

Example: `printf hi | xclip -selection clipboard` in a pane that only a
terminal on another machine focuses puts `hi` on that terminal's clipboard. In
a pane your terminal on the session's machine focuses, the real `xclip` runs.

The real tool runs alone for a read, a clear, a copy that is not plain UTF-8
text, a copy longer than `max-copy-bytes`, and a copy the session does not take
within 5 seconds.

No stand-in sees a tool started by its full path (such as `/usr/bin/xclip`),
through `sudo`, or inside a container; a tool in a folder that a shell startup
file puts before the stand-ins on `PATH`; a tool installed after the session
started; or a pane started before `allow #true` took effect. On macOS, a login
shell such as `zsh -l` runs `path_helper`, which puts `/usr/bin` first, so
`pbcopy` runs from there. The shell koshi starts in a new pane is not a login
shell.

On Windows no stand-in replaces a clipboard tool; `clip.exe` copies on the
session's machine only.

## terminal [#terminal]

**Read by the session** — it starts the programs in the panes, so it decides
what they are told.

| Key             | Value                                                                                                             | Default                                | Since   |
| --------------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------- |
| `term`          | string — the `TERM` value child programs see                                                                      | `"xterm-256color"`                     | ≥ 0.1.0 |
| `colorterm`     | string — the `COLORTERM` value child programs see                                                                 | `"truecolor"`                          | ≥ 0.1.0 |
| `default-shell` | string — the shell to launch                                                                                      | your `$SHELL` (`%COMSPEC%` on Windows) | ≥ 0.1.0 |
| `extended-keys` | `"on-request"` or `"always"` — when pane programs receive enhanced key sequences for keys that share legacy bytes | `"on-request"`                         | ≥ 0.5.0 |

`"on-request"` sends enhanced sequences when a program asks for them.
`"always"` also sends them for keys that share legacy bytes, even when the
program did not ask. See [Keyboard input](/docs/v0.6/using/terminal-input).

<Callout>
  Bash and other programs that use GNU readline can read typed non-ASCII
  characters, such as `é`, as Meta keys when the pane's locale is `C` or `POSIX`
  (no `LC_ALL`, `LC_CTYPE`, or `LANG` set). Set a UTF-8 locale in the pane, such
  as `export LANG=C.UTF-8`, or add `set convert-meta off` and
  `set output-meta on` to `~/.inputrc`.
</Callout>

## logging [#logging]

**Read by every koshi process** for its own log file. koshi writes, in
`logs/` below the state directory, one `koshi-log-<uuid>.log` per session,
named by the session's bare UUID, and `koshi-log-router.log` for the router.
Disabled logging creates no file at all.

| Key                       | Value                                                                            | Default     | Since   |
| ------------------------- | -------------------------------------------------------------------------------- | ----------- | ------- |
| `enabled`                 | boolean — write a log file at all                                                | `#false`    | ≥ 0.1.0 |
| `level`                   | `"info"` \| `"warning"` \| `"error"` — lowest severity written                   | `"warning"` | ≥ 0.1.0 |
| `format`                  | `"pretty"` \| `"json"`                                                           | `"pretty"`  | ≥ 0.1.0 |
| `max-log-file-size-in-mb` | integer, `1` to `1024` — the size in MiB at which a log file moves to `<name>.1` | `4`         | ≥ 0.6.0 |

`info` includes normal lifecycle events, `warning` recoverable problems, and
`error` failures that stop koshi. Each level includes the more severe ones.
Logs store ids and byte counts — never typed or copied text. Crash reports are
separate and always on; see
[Logging and crash reports](/docs/v0.6/using/logging).

## update [#update]

**Read by every installed koshi** for itself.

<Callout type="warn">
  A bad value in this block drops the whole `koshi.kdl` for that launch, not just
  the field.
</Callout>

| Key                   | Value                                                                                                                    | Default  | Since   |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------- | ------- |
| `auto-check`          | boolean — check GitHub for a newer koshi in the background while koshi runs, and offer a newer release at the next start | `#true`  | ≥ 0.1.0 |
| `check-interval-days` | integer — days between checks                                                                                            | `14`     | ≥ 0.1.0 |
| `allow-prerelease`    | boolean — offer pre-release builds too                                                                                   | `#false` | ≥ 0.1.0 |

## allow-beta-features [#allow-beta-features]

**Read by every koshi process** when it starts, so the interactive session and
the `koshi` commands you type all get the same answer.

Some features are finished code that has not been used enough to be turned on
for everyone. They are off unless you say otherwise. Turning this on runs all
of them; there is no per-feature switch.

A beta feature you have not turned on refuses and says so, naming itself and
the line to add:

```text
koshi: `koshi <command>` is a beta feature and did nothing; add a top-level
`allow-beta-features #true` line to koshi.kdl to run it
```

Nothing crashes and nothing is lost; the command exits non-zero having done
nothing.

<Callout>
  No feature in 0.6.0 is marked beta. Every command runs whether the setting is
  on or off.
</Callout>

| Key                   | Value                                    | Default  | Since   |
| --------------------- | ---------------------------------------- | -------- | ------- |
| `allow-beta-features` | boolean — run features still marked beta | `#false` | ≥ 0.2.0 |

## image-support [#image-support]

**Read by your terminal.** Each terminal probes its own capabilities and chooses
Kitty, iTerm2, or Sixel output when the setting is on. A terminal with none of
those protocols keeps the text and the `terminal image unavailable` placeholder.

| Key             | Value                                              | Default | Since   |
| --------------- | -------------------------------------------------- | ------- | ------- |
| `image-support` | boolean — send native image output to the terminal | `#true` | ≥ 0.4.0 |

## auto-close-session [#auto-close-session]

**Read by the session**, from the `koshi.kdl` it saw when it started. A
terminal attaching later cannot change it.

A terminal leaving a session normally leaves the session running with nothing
attached, so `koshi attach` can rejoin it later. Turning this on ends the
session once the last terminal leaves.

koshi counts the terminals after the one that left is gone. If any are still
attached, the session keeps running; only an empty session ends.

Ending it asks every program in the session to stop, waits up to three seconds,
then kills whatever has not exited — long enough for a shell to write its
history and an editor its swap file. On Windows a program cannot be asked to
stop, so everything is killed at once. `koshi kill-session` skips the wait.

Every way of leaving counts: the quit keybinding (<kbd>\<C-q></kbd> by
default), `koshi detach`, closing the terminal, and moving the terminal to
another session with `koshi attach <session>` from inside a pane. On Linux and
macOS, a `SIGHUP`, `SIGTERM`, `SIGINT`, or `SIGQUIT` that koshi does not ignore
is a leaving too; on Windows, Ctrl+C or Ctrl+Break.

These are not a leaving, so the session keeps running even when no terminal is
left:

* a terminal on another machine whose link drops, or that is killed with
  `kill -9`;
* a restart into a new build, such as the one `koshi update` starts. Each
  attached terminal reconnects by itself, and keys typed meanwhile reach the
  restarted session in order.

| Key                  | Value                                                   | Default  | Since   |
| -------------------- | ------------------------------------------------------- | -------- | ------- |
| `auto-close-session` | boolean — end the session when its last terminal leaves | `#false` | ≥ 0.2.0 |

## remote-listen [#remote-listen]

**Read by every koshi process** on the machine holding the sessions.

`remote-listen "0.0.0.0:7654"` names the IP address and port the remote
listener binds, and does nothing else. Use an IP address; hostnames are ignored
with a warning.

<Callout type="warn">
  Writing this line opens no port and makes this machine reachable by nobody. The
  port opens the first time you run `koshi share open`, or answer yes to the
  offer `koshi share grant` makes, and on every start after that.
</Callout>

The router reads this line when it starts. A line you add, change, or remove
while the router runs applies after `koshi restart-servers`. Until then the
router keeps the address it read. `koshi share grant`, `koshi share open`, and
`koshi doctor` name the restart when the two differ.

`allow-other-users` is a separate switch, about other users logged in to this
same machine. Neither key turns the other on.

| Key             | Value                                                      | Default               | Since   |
| --------------- | ---------------------------------------------------------- | --------------------- | ------- |
| `remote-listen` | string — IP address and port the remote TLS listener binds | unset — nothing binds | ≥ 0.3.0 |

See [Remote sessions](/docs/v0.6/remote).

## remote-reconnect [#remote-reconnect]

**Read by your terminal.** It applies only to a terminal viewing a session on
another machine, reached with `koshi attach --remote`.

On, a link that drops dials that machine again — after 1 second, then 2, 4, 8,
and 8 before every dial after that — for up to 120 seconds. While it waits, the
tab strip reads a tag shaped like `RECONNECTING (attempt 4, retry in 8s)`.
Joining again puts back the tab you were on, the focused pane of each tab, the
fullscreened pane of each tab, the scroll offset of each pane, and your
floating panes: which show, where each sits, which are pinned or minimized,
and which one takes your keys. Keys and pastes typed while the link is down are
dropped; the link that comes back reports the terminal's size then. The quit
keybinding leaves at once while the link is down.

Each dial uses the saved server as `koshi remote list` shows it at that
moment, so a `koshi remote edit` or `koshi remote set-secret` made while
attached takes effect at the next dial.

A refusal no dial can change stops the dialing at once, and so does a saved
server that is no longer at that address. Off, a dropped link
ends the terminal, printing how to attach again by hand. A link to a session on
this machine ends the terminal either way.

| Key                | Value                                                                 | Default | Since   |
| ------------------ | --------------------------------------------------------------------- | ------- | ------- |
| `remote-reconnect` | boolean — dial a session on another machine again when the link drops | `#true` | ≥ 0.3.0 |

See [Reconnecting](/docs/v0.6/remote/servers#reconnecting).

## remote-link-timeout-ms [#remote-link-timeout-ms]

**Read by your terminal.** It applies only to a terminal viewing a session on
another machine.

The terminal sends a ping every third of this time, and the session answers
when it has sent nothing for a third of this time. A link over which no byte
arrives for this long has dropped: with `remote-reconnect` on the terminal
dials again, otherwise it ends. The serving machine closes the connection after
the same time without a byte from the terminal, and keeps the terminal's view
for when it joins again. A paste keeps the link for as long as its bytes keep
moving.

Example: with `30000`, a laptop that loses its network shows `RECONNECTING`
at most 30 seconds after the last byte arrived.

A value under `1000` is ignored with the warning `must be at least 1000`.

| Key                      | Value                                                                                               | Default | Since   |
| ------------------------ | --------------------------------------------------------------------------------------------------- | ------- | ------- |
| `remote-link-timeout-ms` | integer, `1000` to `4294967295` — milliseconds with no byte from the other side before a link drops | `30000` | ≥ 0.6.0 |

## max-frames-per-second [#max-frames-per-second]

**Read by your terminal.** The most frames a second the session sends this
terminal; two terminals on one session can set different values. A screen that
changes faster reaches the terminal as its newest state at each frame. The
frame after your own key, paste, or mouse event goes at once.

A value outside `30` to `120` is ignored with the warning
`must be between 30 and 120`. A change applies at the next `koshi attach`.

| Key                     | Value                  | Default | Since   |
| ----------------------- | ---------------------- | ------- | ------- |
| `max-frames-per-second` | integer, `30` to `120` | `60`    | ≥ 0.6.0 |

## reduced-motion [#reduced-motion]

**Read by your terminal.** When `#true`, these happen in one frame instead of
sliding: the move preview, another client's accepted move, a pane you undock,
a floating pane you minimize or restore, the notification center opening and
closing, and a notification toast. A toast still stays for its full time.

| Key              | Value                                                          | Default  | Since   |
| ---------------- | -------------------------------------------------------------- | -------- | ------- |
| `reduced-motion` | boolean — skip pane, notification center, and toast animations | `#false` | ≥ 0.5.0 |

## stay-in-pane-placement-mode-after-placement [#stay-in-pane-placement-mode-after-placement]

**Read by your terminal.** By default, the move mode opened with a key, such
as <kbd>\<C-p></kbd> <kbd>m</kbd>, stays active after an accepted move so
you can move another pane. Set this to `#false` to return to the prior input
mode after each accepted move. A cancelled or rejected move does not change the
mode. The setting also covers floating pane move mode (`MOVE FLOAT`).

A drag started on a pane's grab handle outside the move mode ends with its
drop either way.

| Key                                           | Value                                                                                                                 | Default | Since   |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------- | ------- |
| `stay-in-pane-placement-mode-after-placement` | boolean — keep the move mode active after an accepted move, and floating pane move mode active after a move (≥ 0.6.0) | `#true` | ≥ 0.5.0 |

## allow-other-users [#allow-other-users]

**Read by the session for every connection and every request**, and by every
`koshi` command as it runs — so turning it off shuts those users out without a
restart.

Your sessions are yours alone unless you say otherwise. Turning this on lets
every other user logged in to the same machine list your sessions, attach to
them, and kill them.

This works on Linux and macOS only. On Windows koshi reads the key without an
error and does nothing with it.

Both files have to say so. Your `koshi.kdl` opens your sessions to other users;
their own `koshi.kdl` makes their `koshi` look for sessions that are not
theirs.

<Callout type="warn">
  The programs inside a session keep running as the user who started it, whoever
  attaches. Attaching never hands anyone your account; it hands them a view of,
  and typing into, panes that still run as you.
</Callout>

Turning it on reaches the sessions you start after the change. A running
session keeps the socket it already has until it restarts, and `koshi update`
restarts every session it finds. A session started with `koshi --headless --allow-other-users` keeps other users for its whole life and never reads this
key.

| Key                 | Value                                                                               | Default  | Since   |
| ------------------- | ----------------------------------------------------------------------------------- | -------- | ------- |
| `allow-other-users` | boolean — let other users of this machine reach your sessions; no effect on Windows | `#false` | ≥ 0.3.0 |

See [Sharing with other users](/docs/v0.6/using/other-users).

## shared-sessions-dir [#shared-sessions-dir]

**Read by every koshi process** for itself.

Where the session sockets other users reach are kept. Set it to a directory
every user who shares the machine can enter, such as `/var/run/koshi`. Leave it
out and koshi uses `/tmp/koshi`. On Windows koshi reads the key without an
error and does nothing with it.

Every user who shares the machine has to name the same directory. A user whose
file names a different one looks in that one and finds nobody.

This only says where the sockets go. Nobody else reaches them until
`allow-other-users` is on.

| Key                   | Value                                                                       | Default      | Since   |
| --------------------- | --------------------------------------------------------------------------- | ------------ | ------- |
| `shared-sessions-dir` | string — directory the shared session sockets live in; no effect on Windows | `/tmp/koshi` | ≥ 0.3.0 |

## Every setting at its default [#every-setting-at-its-default]

`default-shell`, `remote-listen`, and `shared-sessions-dir` are commented out —
they have no fixed default. `default-shell` comes from `$SHELL` or `%COMSPEC%`,
`remote-listen` is unset, and the shared sessions directory is `/tmp/koshi`.

```kdl title="koshi.kdl"
// koshi.kdl — the complete default configuration.
version 2

theme "default"
image-support #true
allow-beta-features #false
allow-other-users #false
// remote-listen "0.0.0.0:7654"  // sets the address; opens no port on its own
// shared-sessions-dir "/var/run/koshi"  // optional override
auto-close-session #false
remote-reconnect #true
remote-link-timeout-ms 30000
max-frames-per-second 60
reduced-motion #false
stay-in-pane-placement-mode-after-placement #true

pane {
    min-cols 2
    min-rows 1
    gap 0
}

scrollback {
    max-lines 10000
    max-bytes 33554432       // 32 MiB
    scroll-on-input #true
}

layout {
    new-pane-direction "right"
}

mouse {
    border-resize #true
    scroll-lines 3
    wheel "scroll-scrollback"
}

copy {
    trim-trailing-whitespace #true
}

notifications {
    toast #true
    toast-position "top-right"
    toast-duration-ms 3000
}

pane-clipboard-writes {
    allow #false
    max-copy-bytes 8388608   // 8 MiB
}

terminal {
    term "xterm-256color"
    colorterm "truecolor"
    extended-keys "on-request"
    // default-shell "/bin/zsh"  // optional override
}

logging {
    enabled #false
    level "warning"
    format "pretty"
    max-log-file-size-in-mb 4
}

update {
    auto-check #true
    check-interval-days 14
    allow-prerelease #false
}
```
