Policy configuration¶
In managed environments an administrator can restrict or preconfigure korTTY through a single TOML file, kortty-policy.toml, placed in the installation directory. Users cannot change or bypass it: the file is only read from the admin-writable installation folder, locked settings appear grayed out with a "Managed by your organization" hint, and hand-editing global-settings.xml is undone on the next load. This chapter explains where the file lives, how rules target users, groups and servers, and documents every available parameter with examples.
Note
Without a kortty-policy.toml nothing changes — korTTY behaves exactly as before. The shipped policy/kortty-policy.toml.example is a fully commented template with everything disabled.
File location and security model¶
korTTY loads the policy exclusively from the policy/ folder of its installation directory — the folder that also holds the application jar — and never from ~/.kortty/ or any other user-writable location. A ready-to-copy template ships as policy/kortty-policy.toml.example; copy it to kortty-policy.toml in the same folder and restart korTTY (there is no hot reload).
| Platform | Policy file location |
|---|---|
| macOS | /Applications/KorTTY.app/Contents/app/policy/kortty-policy.toml |
| Windows | C:\Program Files\KorTTY\app\policy\kortty-policy.toml |
| Linux (deb/rpm) | /opt/kortty/lib/app/policy/kortty-policy.toml |
The enforcement model relies on the operating system's file permissions: the installation directory must be writable only by administrators, which is the default for the locations above. korTTY additionally logs a warning when the active policy file is writable by the current user. During development (never in a packaged installation) a policy can be tested with -Dkortty.policy.file=/path/to/policy.toml.
If the file exists but cannot be parsed or contains an invalid value, korTTY starts in a fail-safe lockdown: every policy-controllable feature is denied, no server connection is allowed, every terminal paste with a line break asks for confirmation, programs cannot put text on the clipboard with OSC 52, the previous session is not reopened at startup, the output of terminal panes is not saved for it, and a startup dialog names the file and the exact error position. A typo can therefore never silently disable enforcement. One invalid rule rejects the entire file; unknown keys only produce log warnings, so a policy written for a newer korTTY does not lock out users of an older version. This holds for [rule.ai-memory] too: an unknown key there is logged and ignored, so check the log after you change that table.
Users, groups and rule precedence¶
Rules target the current OS login name (matched lowercase). Group membership comes from two sources at once: groups defined in the policy's [groups] table, and the user's OS-level group memberships. On domain-joined Windows machines the OS groups include Active Directory groups, so rules can target AD groups directly — both fully qualified (ACME\Operations) and as the bare name (operations) — without any directory server configuration.
Each [[rule]] block optionally names users and/or groups; a rule naming neither applies to every user. Per setting, the most specific tier that sets it wins: user beats group beats everyone. That is the familiar group-policy pattern — lock everything down in a baseline rule for everyone, then relax individual settings for a trusted group. When several rules of the same tier set the same key, the most restrictive value applies (deny over read-only over confirm over allow); for server rules, a connection must pass every applicable restriction of the winning tier. The order of rules in the file has no significance.
[meta]
schema-version = 1
organization = "ACME Corp"
[groups]
devs = ["alice", "bob"]
[[rule]] # applies to ALL users
name = "company-baseline"
[rule.features]
ai-agent = "deny"
[[rule]] # group tier: relaxes the baseline for ops
name = "ops-exception"
groups = ["ops", "ACME\\Operations"] # policy group OR OS/AD group
[rule.features]
ai-agent = "allow"
ai-agent-execution = "confirm"
[[rule]] # user tier: beats eve's group
users = ["eve"]
[rule.features]
ai-agent = "deny"
Server access control¶
The [rule.servers] table restricts which servers a user may connect to — as an allow-list (mode = "allow": only listed servers are reachable) or a deny-list (mode = "deny": listed servers are blocked). The restriction is enforced centrally for every connection path: saved connections, QuickConnect, split panes (to a new connection or the same server), session restore, SFTP, teamwork-shared connections, AI swarm targets and scheduled jobs, including the jump host of a connection. Blocked connections stay visible in the connection manager but are grayed out with a lock marker, and any connect attempt shows a clear policy message.
Open Group in Quick Connect skips every connection of the group whose server or jump server is blocked and names them all in one policy message; the other connections of the group still open. Connect in the Connection Manager, a tab's Duplicate and Reopen Closed Tab check the policy before they ask for a password, a temporary SSH key or the master password, and an open tab checks it again on every connect and reconnect attempt, because editing a saved connection also changes the tabs already opened from it. If such an edit points a tab at a blocked server or jump server, its terminal shows the policy message and korTTY stops trying, including automatic reconnects.
Local shells and serial connections have no server and are not checked against [rule.servers]; serial ports are governed by serial-connections under [rule.features] instead.
Patterns match the host string exactly as configured in the connection — korTTY never resolves DNS for policy checks, so host names and IP addresses are separate namespaces: if a server is reachable both ways, list both.
| Pattern form | Example | Matches |
|---|---|---|
| Exact host name | db01.acme.com |
that host, case-insensitive, any port |
| Host-name glob | *.prod.acme.com, web-??.acme.com |
* = any characters, ? = one character |
| Host with port | vault.acme.com:22 |
only connections to that port |
| Single IP address | 192.168.10.42, 2001:db8::1 |
that address (IPv4 or IPv6) |
| IP with port | 192.168.10.42:22, [2001:db8::1]:22 |
IPv6 ports require the bracket form |
| CIDR network | 10.99.0.0/16, 2001:db8::/32 |
every address in the network |
| IP range | 10.20.0.100-10.20.0.199 |
inclusive from–to range |
[[rule]]
[rule.servers]
mode = "deny"
hosts = ["*.prod.acme.com", "vault.acme.com:22", "192.168.10.42", "10.99.0.0/16", "10.20.0.100-10.20.0.199"]
Parameter reference¶
[meta]¶
| Key | Type | Values | Required | Effect |
|---|---|---|---|---|
schema-version |
integer | 1 |
yes | Rejected (lockdown) when this korTTY does not understand the version |
organization |
string | free text | no | Shown in every "Managed by your organization" hint and dialog |
[groups]¶
| Key | Type | Effect |
|---|---|---|
<group-name> |
array of user names | Defines a policy group; rules referencing the name target its members. OS/AD groups match automatically and need no entry here |
[[rule]] scope¶
| Key | Type | Effect |
|---|---|---|
name |
string | Optional label used in log messages |
users |
array of user names | Targets the listed OS login names (user tier) |
groups |
array of group names | Targets members of policy groups and/or OS/AD groups (group tier); a rule with neither users nor groups applies to everyone |
[rule.servers]¶
| Key | Type | Values | Effect |
|---|---|---|---|
mode |
string | allow, deny |
Allow-list or deny-list semantics for hosts |
hosts |
array of patterns | see table above | Non-empty list of server patterns |
[rule.features]¶
| Key | Type | Values | Restricts |
|---|---|---|---|
ai |
string | allow, deny |
Master switch: deny disables every AI capability at once, and scheduled AI Agent and AI Swarm jobs end as blocked before they connect |
ai-agent |
string | allow, deny |
AI Agent (menu, terminal context menu, keyboard shortcut, headless job runs). deny also blocks scheduled AI Agent and AI Swarm jobs before they connect, because swarm agents are AI agents |
ai-chat |
string | allow, deny |
AI chat, Saved Chats and the terminal-selection AI actions. deny also greys out Insert and Run on the code blocks of a chat |
ai-swarm |
string | allow, deny |
AI Swarm, including scheduled swarm jobs, which end as blocked before they connect |
ai-planning |
string | allow, deny |
AI Planning |
teamwork |
string | allow, deny |
Teamwork shared-connections sync (service is not started, menu locked) |
plugins |
string | allow, deny |
Plugin loading and the Plugins menu (e.g. terminal effects) |
session-journal |
string | allow, deny |
The session journal: capture, journal bar, manager, viewer and exports. Not chained to ai — with AI denied the journal still records raw activity |
control-api |
string | allow, deny |
The control API and its kortty-cli client: deny stops the listener, locks the Settings checkbox and forces the setting off. Not chained to ai — the API is local automation, not an AI capability |
terminal-triggers |
string | allow, deny |
Highlight rules that act when their pattern appears in terminal output (a desktop notification, or running a snippet in the pane): deny stops every such action, also a snippet the user already allowed for a connection, locks Run actions of highlight rules (notifications, snippets) in Settings → Terminal in the off position and keeps the rule editor from giving a rule an action. Keyword highlighting itself is not affected. allow locks the setting on |
multi-exec |
string | allow, deny |
Typing into several terminals at once: multi-exec across tabs and windows and a tab's broadcast mode. deny locks the include items of View → Multi-exec, View → Panes → Broadcast, the Multi-exec and Broadcast Mode entries of the tab, pane and Dashboard context menus, and their command palette and shortcut entries; no pane can join from anywhere. Stop Multi-exec and switching broadcast mode off keep working. There is no Settings control, so allow changes nothing |
file-transfer |
string | allow, deny |
Copying files between this computer and a server. deny greys out the SFTP manager's Upload and Download buttons and the transfer list's Retry, rejects drags onto either panel that would copy to or from the server, stops server files being dragged out of the window, rejects files dragged onto a terminal pane while they are dragged, greys out the Snippet Manager's Copy as file(s) to terminal directory and Copy folder to terminal directory for SSH tabs, greys out the upload and download buttons of the terminal's remote files sidebar and rejects drops onto it and drags out of it, and makes the JobScheduler's SFTP upload, download and sync actions and its rsync action fail with the policy message before they connect. Opening the SFTP manager (also with Open SFTP Here on a terminal's session), browsing (also in the remote files sidebar) and remote-only operations (rename, delete, permissions, owner, archives, search, remote copy) keep working, and loading a file into the snippet editor stays under load-into-snippet-editor. Edit in External Editor needs a local copy, so deny greys it out too. Commands such as scp or rsync typed into a terminal cannot be blocked by korTTY. There is no Settings control, so allow changes nothing |
sftp-sudo-edit |
string | allow, deny |
Editing server files as root from the SFTP manager. deny greys out Edit as Root (sudo)... in the right-click menu and the remote Edit menu. It is also greyed out while file-transfer is denied or load-into-snippet-editor is read-only or deny, because an edit as root always needs a local copy and always writes back. There is no Settings control, so allow changes nothing |
job-webhooks |
string | allow, deny |
JobScheduler webhook notifications to Slack, Teams or a generic JSON receiver. deny stops every webhook delivery of a run and every test message before anything is decrypted or sent, and the job journal records notification blocked by policy for each target; desktop notifications are not affected. Which hosts webhooks may reach is set by webhook-host-allowlist in [rule.job-scheduler]. There is no Settings control, so allow changes nothing |
mcp-server |
string | allow, deny |
korTTY as an MCP server: Control API clients that declare themselves MCP clients. deny refuses every such connection with blocked_by_policy and forces both the MCP server switch and Allow write tools off; plain Control API clients and kortty-cli keep working. It also needs control-api. allow leaves both switches with the user and never switches them on |
incognito-sessions |
string | allow, deny |
Incognito sessions, whose log, journal, recording, Recently Closed entry and restore korTTY leaves out. deny greys out Incognito session in the connection editor and File → New Incognito Session…, and every session, also of a connection marked incognito, is a normal one. A session journal the policy enforces is written for incognito sessions too. There is no Settings control, so allow changes nothing |
serial-connections |
string | allow, deny |
Serial connections to a serial port of the computer. A serial port has no host, so [rule.servers] cannot cover it; deny blocks them as a whole: the connection editor and Quick Connect no longer offer Serial for new connections, and every attempt to open a serial port — a saved or imported serial connection, a restored tab, a reconnect — is refused with Your organization's policy does not allow serial connections. Saved serial connections stay stored. Lockdown denies it too. There is no Settings control, so allow changes nothing |
desktop-notifications |
string | allow, deny |
Every desktop notification korTTY shows: coding agents, the terminal bell, finished commands, OSC 9 / OSC 777 requests, AI runs, activity and silence, highlight-rule notifications, JobScheduler jobs and the notify call of the control API. deny shows none of them and forces the five notification switches in Settings → Messaging off and locks them; the marks on tabs and the app icon badge stay, and webhooks stay under job-webhooks. allow leaves every switch with the user and never switches one on |
ai-agent-execution |
string | allow, confirm, read-only |
confirm forces interactive approval of every mutating command set and defeats the auto-approve option; read-only lets the agent plan and chat but never execute commands, and greys out Run on the code blocks of an AI chat (Insert stays, since it runs nothing). Scheduled jobs: under confirm an AI Agent or AI Swarm job blocks every server-changing command, whatever its AI agent may change the server without runtime confirmation says, because nobody is there to approve it; under read-only both AI job types end as blocked before they connect. A job is also blocked up front when ai or ai-agent is denied, and a swarm job when ai-swarm is denied |
ai-memory |
string | allow, deny |
The korTTY memory: the MEMORY.md files, the reference files of other tools and the learned rules. deny turns all of it off: no memory is sent with any request, no rule is learned, the user's files are neither read nor written, and organisation memory is not sent either. allow keeps the user's choice but locks the Use memory switch in the position the user has. The details are set under [rule.ai-memory] |
Naming a feature takes it over, whichever way you decide it
A policy file that mentions a key locks the corresponding control, in the position the policy chose. Writing control-api = "allow" therefore locks the checkbox on — users can no longer switch the API off themselves. Leave the key out entirely to leave the choice with the user; write deny to take it away.
[rule.security]¶
| Key | Type | Values | Effect |
|---|---|---|---|
require-master-password |
boolean | true |
Forces the master-password gate at startup; the setting is locked |
enforce-host-key-check |
boolean | true |
SSH host key verification cannot be disabled anywhere — globally, per group or per connection — and trusted host keys cannot be removed or replaced in korTTY; importing an OpenSSH known_hosts file stays available because it only adds keys for hosts korTTY does not trust yet |
allow-telemetry |
boolean | false |
Forbids anonymous usage statistics |
allow-terminal-recording |
boolean | false |
Forbids terminal session recording, including the session-level toggle |
allow-port-forwarding |
boolean | false |
Never opens the SSH tunnels configured on connections (local, remote and dynamic port forwarding); the tab's status bar says they are disabled by your organization. A jump server hop is not affected |
clipboard-mode |
string | system, internal |
internal confines korTTY to its own in-memory clipboard — see below |
allow-osc52-clipboard-write |
boolean | false |
Programs in a terminal can never put text on the clipboard with OSC 52, whatever Settings → Terminal says: the setting is forced off and Let programs in the terminal copy text to the clipboard (OSC 52) is locked with the "Managed by your organization" hint, and korTTY checks the policy again on every write. It also governs writes with OSC 5522. true leaves the choice, which is off by default, to the user and locks nothing |
allow-osc5522-clipboard-read |
boolean | false |
Programs in a terminal can never read the clipboard with OSC 5522, kitty's clipboard protocol, even with the user's consent: Let programs in the terminal ask to read the clipboard (OSC 5522) is forced off and locked, no prompt opens and pastes never go out as paste events. true leaves the choice, which is off by default and asks before every read, to the user |
[rule.teamwork], [rule.snippets], [rule.ai-profiles]¶
| Key | Type | Values | Effect |
|---|---|---|---|
allow-custom-sources |
boolean | false |
Users cannot add teamwork sources; only [[teamwork-source]] entries remain |
allow-custom-script-headers |
boolean | false |
Users cannot create script headers; only [[script-header]] entries remain |
analysis-max-stored-content-bytes |
integer | ≥ 0 (bytes of UTF-8) | Upper bound for the script text stored with each Full code analysis; 0 forbids storing script text — see below |
allow-create |
boolean | false |
Users cannot create AI profiles (buttons and wizard are locked) |
allow-edit |
boolean | false |
Users cannot edit their existing AI profiles either |
allow-internet |
boolean | false |
Forbids every AI internet-access mode — see below |
allow-internet = false cuts AI web access at three levels
The internet access dropdown of an AI profile selects a web-search or MCP browsing backend. Forbidding it is enforced in three places, because a single one would leave a gap:
- Stored settings — every profile's mode is reset to Disabled whenever the settings are
loaded or saved, so a value from before the policy arrived does not survive, and neither does
a hand-edit of
global-settings.xml. - The interface — the dropdown is locked with the "managed by your organization" hint in the AI Manager.
- Every AI request — korTTY refuses to build a service for a profile whose mode is enabled, which closes the window between two clamps. The request fails with a policy message rather than quietly answering without the web tool the profile asked for.
This is about the search backends only. It does not stop a cloud AI profile from reaching its
own provider — for that, deny the ai feature or provision [[ai-profile]] entries that point
at an internal endpoint.
Stored script text of analyses¶
A Full code analysis stores the script text it worked on — the analysed script, the text an apply started from, the proposed result and the accepted text — so the code preview, View changes, the script export, the report appendix, Restore intermediate state and resuming after a restart keep working. Users choose the size themselves under Settings → Snippet Editor → Stored script size per analysis (default 1 MB, maximum 5 MB, or Off); analysis-max-stored-content-bytes in [rule.snippets] is an upper bound on top of that choice:
[[rule]]
name = "Keep script text small"
[rule.snippets]
analysis-max-stored-content-bytes = 524_288 # at most 512 KB per stored text
[[rule]]
name = "Compliance team: no script text in analyses"
groups = ["compliance"]
[rule.snippets]
analysis-max-stored-content-bytes = 0 # findings and reports only
- Effective limit — The smaller of the user's own value (default 1 MB), the fixed 5 MB maximum and this key. A value above 5 MB therefore changes nothing, and a user's lower value stays in force. Any positive value is accepted, also one below the 256 KB minimum users can pick.
0— No script text is stored at all: the analysis keeps its findings, summary, diagram and reports, but not the script, so View changes, the older entries' code preview, the script export, the report appendix, Restore intermediate state and resuming after a restart are unavailable, and the panel says the organization does not allow storing script text. The analysis itself, applying findings and the immediate change review in the editor work as before. Text stored earlier is dropped from the stored analyses the next time they are written.- Settings dialog — The dropdown only offers sizes up to the cap, shows the cap beneath it and is locked (with the "Managed by your organization" hint) when the value is
0. The user's own choice is not overwritten by the cap: it applies again when the policy is lifted. - Tiers — Like every setting the most specific tier that sets the key wins (user, then group, then all); rules of the same tier take the smaller value, so
0beats every positive cap. The key is not a0-means-unlimited value. - Invalid values — A negative number or a non-integer (for example
"5 MB") is a policy error like any other malformed value, so the whole file is rejected and the lockdown fallback applies; the fallback forbids storing script text too. Write bytes, optionally with TOML digit separators (5_242_880).
[rule.ai-memory]¶
The [rule.ai-memory] table sets how the korTTY memory may be used. Every key is optional; a key the policy does not set keeps the user's own choice. A value outside the listed ones rejects the whole file. An unknown key is a warning in the log and is ignored, like in every other table. As with the other keys, the most specific tier that sets a key wins (user, then group, then all); inside one tier the strictest value applies: disabled over read-only over editable, propose under off, never over always over auto, the smaller token cap, and false over true for the switches.
| Key | Type | Values | Effect |
|---|---|---|---|
user-memory |
string | editable, read-only, disabled |
read-only reads the user's MEMORY.md files into prompts but never writes them; disabled neither reads nor writes them, and organisation memory is still sent |
learning |
string | propose, off |
off finds no rules in conversations and proposes none; the Learning setting of the Memory tab is set to the value and locked |
skill-creation |
boolean | true, false |
The switch korTTY may create skills and save learned rules in them is set to the value and locked. false means that no rule goes into a skill, new or existing; every rule goes into a memory instead |
external-files |
string | allow, deny |
Whether reference files of other tools (CLAUDE.md, AGENTS.md and similar) may be read; deny reads none of them |
external-file-roots |
array of paths | absolute or ~/ paths |
The folders external files must lie in; an empty list allows any folder |
max-tokens |
integer | 300 to 4000 |
The memory budget per AI request; the smaller of this and the user's own setting applies |
triage |
string | auto, always, never |
When the agent offers solution options: auto judges each request, always offers options for every run, never runs requests as asked. The user's Estimate complexity setting is set to the value and locked |
statistics |
boolean | true, false |
Whether skill statistics are recorded: false records none. Both values set the Record statistics switch and lock it |
Organisation memory¶
[[organization-memory]] blocks hold rules for the AI that users cannot change. Each block has a title and a content text. korTTY sends the blocks in full, ahead of the user's own memory, with every AI request of every user, and does not cut them to the token budget. A block whose title or content is longer than 8000 characters, or that the secret guard refuses, is dropped with a warning that names the field and the line, never the value. The file holds at most 20 blocks and 16000 characters in all; a block that would exceed either limit is dropped with a warning, and the blocks after it are still checked.
Organisation memory is sent only while the policy allows the AI features and does not deny ai-memory. The user's own Use memory switch and the user-memory key do not stop it.
[rule.ai-runtime]¶
| Key | Type | Values | Effect |
|---|---|---|---|
allow-runtime-downloads |
boolean | false |
No llama.cpp/MLX runtime downloads or update checks |
allow-model-downloads |
boolean | false |
The Hugging Face model browser and downloads are disabled |
allow-user-models |
boolean | false |
Only admin-provisioned [[ai-runtime.model]] entries can be loaded by embedded AI profiles |
[rule.updates]¶
| Key | Type | Values | Effect |
|---|---|---|---|
enabled |
boolean | false |
Disables the automatic update check and the manual check in the About dialog |
feed-url |
string | http(s) URL | Update checks query this endpoint instead of GitHub; it must return the same JSON shape as the GitHub releases/latest API (tag_name, assets[] with name and browser_download_url) |
[rule.backup]¶
Sets and locks the automatic backup in Settings → Backup. Each key forces the matching setting; the user sees it with the "managed by your organization" hint. The backup itself keeps the user's encryption mode and credential or GPG key, and it is never written unencrypted.
| Key | Type | Values | Effect |
|---|---|---|---|
auto-interval |
string | off, daily, weekly, monthly |
Forces the schedule; off switches the automatic backup off for the user. When several same-tier rules set it, the stricter value wins: off, then the more frequent interval |
auto-directory |
string | absolute or ~/-relative path |
Forces the folder the automatic backup is written to. The folder must exist on the user's machine; korTTY does not create it and reports a missing folder in the status line and the log. On a same-tier tie the lexicographically smallest path wins |
[rule.terminal]¶
| Key | Type | Values | Effect |
|---|---|---|---|
load-into-snippet-editor |
string | allow, read-only, deny |
read-only keeps loading remote files into the snippet editor but forbids writing back to the target system; deny removes the feature entirely, including opening file paths and file: links from terminal output (Links in terminal output). The same mode applies to server files in the SFTP Manager: read-only opens them without Overwrite file and Save as..., and deny disables Edit in Snippet Editor and Open image for them; local files are not affected. Edit in External Editor follows the same mode: read-only opens the file without uploading saves, and deny disables it |
paste-warning |
string | off, unless-bracketed, always |
The least a paste warning may ask. It is a floor, applied on top of Settings → Terminal → Paste protection, a connection's own paste warning and the paste warning of a teamwork connection, so none of them can ask less often; one that asks more often keeps doing so. The stored setting is raised to the floor, and the dropdown carries the "Managed by your organization" hint and offers only the floor and stricter choices; with always it is locked. When several rules of the same tier set it, the stricter value wins (always over unless-bracketed over off) |
session-restore |
string | off, ask, auto |
Sets what korTTY does at startup with the previous session's windows and tabs (Settings → Window → Session Restore) and locks the dropdown. off reopens nothing by itself; File → Restore Previous Session stays available. When several rules of the same tier set it, the value that opens fewer connections by itself wins (off over ask over auto) |
session-restore-output |
boolean | true, false |
Sets and locks Also restore the output of each terminal pane (encrypted) in Settings → Window → Session Restore, together with Lines of output per pane. false switches it off, which deletes the output saved so far at the next start; true switches it on, and the output is still saved only while a master password exists and the vault is unlocked. When several rules of the same tier set it, false wins |
[[rule]]
[rule.features]
multi-exec = "deny"
[rule.security]
allow-osc52-clipboard-write = false
allow-osc5522-clipboard-read = false
[rule.terminal]
paste-warning = "always"
session-restore = "ask"
session-restore-output = false
[rule.sftp]¶
| Key | Type | Values | Effect |
|---|---|---|---|
max-parallel-transfers |
integer | 1 to 8 |
The most files the SFTP manager copies at once. A cap on Parallel transfers in Settings → SFTP Manager: a higher stored value is lowered to it and the spinner goes no higher, while users may still choose fewer. When several rules of the same tier set it, the smallest value wins |
conflict-default |
string | ask, skip, overwrite |
Sets and locks When the target already exists in Settings → SFTP Manager: what an upload or download does with a file that is already there. When several rules of the same tier set it, the value that loses less data wins (ask over skip over overwrite) |
[[rule]]
[rule.features]
file-transfer = "allow"
sftp-sudo-edit = "deny"
[rule.sftp]
max-parallel-transfers = 2
conflict-default = "ask"
A malformed policy file falls back to lockdown, which denies file-transfer and sftp-sudo-edit like every other feature.
Archives created on the server by the SFTP manager never carry a password, whatever the policy says: zip and 7z take a password only on their command line or from a terminal, and on the command line every other user of the server can read it in the process list. Password-protected archives on the server are likewise refused by Extract Here.... Archives created on the user's own computer can still have a password, so no policy key is needed to keep server passwords out of process lists.
[rule.isolation]¶
| Key | Type | Values | Effect |
|---|---|---|---|
minimum |
string | none, process, sandbox |
The least session isolation every terminal session must have. A floor on top of Settings → Security → Session isolation, folder levels, a connection's own level and teamwork connections; a level above it keeps applying. The stored default is raised to it, and the levels below it are greyed out in Settings, the connection editor and the folder menu. A session that cannot get the minimum — because its connection type cannot be isolated that far in this version, or because sandbox is demanded and the computer has no working sandbox — is not opened, and the user sees why. With sandbox, a sandbox that is missing is never replaced by a plain process. When several rules of the same tier set it, the stricter value wins (sandbox over process over none). Lockdown does not set it |
[rule.job-scheduler]¶
| Key | Type | Values | Effect |
|---|---|---|---|
webhook-host-allowlist |
array of strings | host names, e.g. ["hooks.slack.com"] |
The only hosts JobScheduler webhooks may be sent to. An entry allows its own host and every host below it, matched on whole labels: hooks.slack.com allows hooks.slack.com and eu.hooks.slack.com, but not evilhooks.slack.com or hooks.slack.com.attacker.net. A leading *. or . is accepted and means the same. Entries are bare host names: a scheme, path, port or user name is an error. Host names are compared as written in the webhook URL and never resolved through DNS. An empty array allows any host; leaving the key out leaves webhooks unrestricted too. When several rules of the same tier set a list, a host must be allowed by each of them |
The check runs right before every send, the Send test button included. A webhook to a host outside the list is not sent, and the job journal records notification blocked by policy with the job, the target's name and the host, never the URL. While job-webhooks is denied, the Notifications section of a job that has webhook targets ticked says that they receive nothing.
[[rule]]
[rule.features]
job-webhooks = "allow"
[rule.job-scheduler]
webhook-host-allowlist = ["hooks.slack.com", "logic.azure.com", "api.powerplatform.com"]
A malformed policy file falls back to lockdown, which denies job-webhooks like every other feature.
[rule.logging]¶
The terminal-* keys below apply to terminal output logs. Unset values leave the connection preference in effect. They resolve per field using the existing rule tiers; at the same tier false wins for username, while directory choices resolve in the order none, relative, full. The logger applies them at startup as well as locking the connection editor controls. Unknown working directories remain empty. These keys do not enable logging on their own.
korTTY rotates its log daily (kortty.YYYY-MM-DD.log); this table controls where the files go, how long they live, how they are formatted and how rotation is capped. Directory and retention are forced into the corresponding (then locked) user settings; format, compression and the rotation caps have no user setting and act directly. The configuration is applied before the very first log line of a start, so even startup logging follows the admin's scheme.
| Key | Type | Values | Effect |
|---|---|---|---|
directory |
string | absolute or ~/-relative path |
Log directory (default ~/.kortty/logs); the setting is locked |
retention-days |
integer | 0 = keep forever |
Rotated logs older than N days are deleted; the setting is locked |
compress |
boolean | default true |
Gzip rotated logs after one day |
format |
string | text, json |
json writes one structured JSON object per event (logback JSON encoder — convenient for SIEM/central log collection) |
rotation-max-files |
integer | 0 = unlimited |
Keep at most N rotated daily files |
rotation-total-size-mb |
integer | 0 = uncapped |
Cap the total size of all rotated files (oldest deleted first) |
terminal-include-username |
boolean | true, false |
Mandate whether terminal log entries include the SSH username; locks the connection setting |
terminal-directory-mode |
string | none, full, relative |
Mandate the terminal log working-directory field; relative uses the first session-reported directory, also recorded as directoryBase; locks the connection setting |
[[rule]]
[rule.logging]
directory = "/var/log/kortty"
retention-days = 30
compress = true
format = "json"
rotation-max-files = 14
rotation-total-size-mb = 512
Note
When several same-tier rules configure logging, each key resolves separately: shorter retention and tighter caps win, compression on wins, json wins over text. In practice, put the logging configuration into a single all-users rule. The chosen directory must be writable by the user running korTTY.
[rule.session-journal]¶
Mandates for the session journal. Forced values lock the corresponding controls in the journal options, the settings dialog and the connection editor with a "managed by your organization" hint.
| Key | Type | Values | Effect |
|---|---|---|---|
enforced |
boolean | true |
A journal is written for every connection, regardless of the per-connection setting; users cannot stop it and the enable switch is locked on |
log-format |
string | json (default), xml, yaml |
Forces the capture-log format for new journals |
ai-max-lines |
integer | 0 = context fill |
Forces the AI evaluation window (max terminal lines per summary) |
storage-path |
string | absolute path | Forces the journal storage directory; the setting is locked |
allow-rename |
boolean | false |
Journals cannot be renamed in the manager |
allow-delete |
boolean | false |
Journals cannot be deleted in the manager |
name-template |
string | template | Initial journal title, with {connection}, {host}, {user}, {date} and {time} placeholders |
ai-title |
boolean | true |
The closing AI title is generated regardless of the user setting |
ai-screenshot-analysis |
boolean | true / false |
true forces the AI screenshot analysis on, false forbids it — including the manual per-screenshot run; the journal option is locked either way |
ai-ask |
boolean | false |
Forbids on-demand AI over journal content: the viewer's Q&A panel and the manager's cross-journal AI search disappear; AI summaries are unaffected |
clickable-links |
boolean | false |
Web addresses on journal pages stay plain text — in notes and in AI summaries — and the journal viewer opens no link; the user setting Make web addresses in journals clickable is locked off |
max-log-parts |
integer | ≥ 1 | Caps the number of rotated capture-log parts per journal; the effective limit is the minimum of this cap and the per-connection setting, and the connection editor's spinner is clamped to it |
automation-allowed |
boolean | false |
Forbids the session journals of JobScheduler and AI Swarm runs; the "session journal per run" controls are disabled. Interactive journals are unaffected |
automation-max-retention-days |
integer | ≥ 1 | Automation journals are deleted at most this many days after their run, whatever the user chose ("never" is not available); enforced even when allow-delete = false |
automation-max-storage-mb |
integer | ≥ 1 | Caps the disk space of one job's (or the AI Swarm's) journals; the oldest runs are deleted first; enforced even when allow-delete = false |
automation-max-journals |
integer | ≥ 1 | Caps the number of kept runs per job (or AI Swarm); enforced even when allow-delete = false |
[[rule]]
[rule.features]
session-journal = "allow"
[rule.session-journal]
enforced = true
log-format = "json"
storage-path = "/srv/audit/kortty-journals"
allow-delete = false
name-template = "{connection} {date} {time} ({user})"
Note
enforced mandates capture, not AI: with AI denied or unavailable the enforced journal records raw activity entries. When several same-tier rules configure the journal, enforced and ai-title resolve to true if any rule sets them, allow-rename/allow-delete to false if any rule forbids them, ai-screenshot-analysis and ai-ask to off if any rule switches them off, the line cap resolves to the tighter value (0 counts as unlimited), and max-log-parts resolves to the lower cap. automation-allowed resolves to false if any rule forbids it, and the three automation-max-* caps resolve to the lower value. With allow-delete = false korTTY never deletes an automation journal because of a user setting (keep mode, duplicates, retention, limits) — only the automation-max-* caps still delete.
[[rule.session-journal.replace]]¶
Automatic search-and-replace in every journal — the way to keep a whole category of secret out of the transcript, instead of relying on the user to notice it. Each entry is one rule, and a rule may use a regular expression.
korTTY applies the same rules to a terminal selection, and to a file attached to it, before they are sent to an AI profile other than an integrated model or a trusted local endpoint; see Masking secrets before sending.
| Key | Type | Default | Effect |
|---|---|---|---|
pattern |
string | required | The text to find. With regex enabled it is a regular expression |
replacement |
string | *** |
The text that replaces each match. In a regex rule $1 inserts a captured group |
regex |
boolean | false |
Treats pattern as a regular expression |
ignore-case |
boolean | false |
Matches every casing |
label |
string | – | Description for the administrator; korTTY only counts the rules in its UI hint |
[[rule]]
[rule.session-journal]
enforced = true
[[rule.session-journal.replace]]
pattern = "AKIA[0-9A-Z]{16}"
replacement = "***AWS-ACCESS-KEY***"
regex = true
label = "AWS access keys"
[[rule.session-journal.replace]]
pattern = "(?i)bearer\\s+[A-Za-z0-9._-]{20,}"
replacement = "Bearer ***"
regex = true
label = "Bearer tokens"
[[rule.session-journal.replace]]
pattern = "vpn.internal.acme.corp"
replacement = "<internal-host>"
ignore-case = true
The rules run on the capture thread, before a line is written, so a matching text never reaches the log file at all, and they are applied to AI summaries and user notes as well. Unlike the other keys, replacement rules are merged across every matching rule and every tier rather than the highest tier winning: more redaction is the more restrictive outcome, so a user- or group-specific rule can only add patterns, never switch the organization-wide ones off. Duplicate entries are collapsed.
Warning
A pattern that is not a valid regular expression is a policy error, not a warning — the file is rejected. A rule that silently matches nothing is worse than one the administrator has to fix, because the redaction would appear to be in place.
Note
Only journals written while the rule is in force are covered; existing journals are not rewritten retroactively. Users can clean those up with the viewer's Search & replace.
Admin-provided objects¶
These top-level tables define objects that appear read-only for every user, marked "Provided by your organization". They are rebuilt from the policy on every start and are never written into the user's configuration files — removing them from the policy removes them from korTTY.
| Table | Keys | Notes |
|---|---|---|
[[script-header]] |
name, content |
Immutable script header in the snippet system's Script-Header category |
[[ai-profile]] |
id, name, provider, endpoint, model, api-key-encrypted |
id must start with policy-; provider is one of anthropic, openai-compatible, lm-studio, embedded-llama, embedded-mlx (embedded providers read model as the local model id) |
[[ai-runtime.model]] |
name, runtime, source |
runtime is llama or mlx; source is an absolute local/UNC path, or for GGUF models an http(s) URL that korTTY downloads once at startup |
[[teamwork-source]] |
name, type, url |
type is git or shared-file; injected as a read-only teamwork source |
Internal clipboard mode¶
With clipboard-mode = "internal" korTTY detaches from the operating system clipboard entirely and uses its own in-memory clipboard instead: text copied in another application cannot be pasted anywhere in korTTY, and text copied in korTTY never reaches the OS clipboard — while copy, cut and paste within korTTY keep working everywhere, because terminals, SFTP views, the snippet editor and all dialogs share the same internal buffer. Because this is pure application logic and no OS clipboard isolation API is involved, it behaves identically on macOS, Windows and Linux, including the X11 primary selection (middle-click paste): pasting middle-click content from other applications is blocked, middle-click paste of korTTY-internal copies keeps working.
The mode covers the terminal (shortcuts, context menu, middle-click, text dropped onto it from another application, which is refused, and text that a program in the terminal copies with OSC 52 or OSC 5522 where Settings → Terminal allows it, which lands in the internal clipboard only; a program that reads with OSC 5522 gets the internal clipboard), the code editor, all copy buttons, the ${clipboard} snippet variable, and the copy/cut/paste shortcuts of plain input fields. Copying images (AI-generated pictures, diagram exports) is unavailable in internal mode, since an image can only be shared via the OS clipboard.
Scope
The internal clipboard is a policy tool against casual data transfer through the clipboard, not a hard air gap: a user can still read text on screen. The right-click Paste entry of plain text fields is provided by the UI toolkit and may still access the OS clipboard — the keyboard shortcut and every korTTY-provided menu are covered.
Encrypted API keys¶
An AI profile's API key never appears in plain text in the policy. The administrator encrypts it once from a terminal — the command prints a kortty-enc:v1: value for the api-key-encrypted key:
Users see only "API key provided by your organization" in the profile; the key is decrypted in memory at the moment a request is made.
Security scope
The envelope uses AES-256-GCM with an application-wide key, so it protects against casual disclosure (shoulder surfing, config diffs, backups) and detects tampering — it is not hard secrecy, since anyone with the korTTY binary could recover the application key. The installation directory's OS permissions remain the actual security boundary; prefer per-user keys via the normal profile flow when that boundary is not enough.
Troubleshooting¶
| Symptom | Cause and remedy |
|---|---|
| Startup dialog "Organization policy could not be loaded" | The policy file has a syntax error or invalid value; the dialog and the log name the exact position. korTTY stays in fail-safe lockdown until the file is fixed |
| Policy seems ignored | The file is not named kortty-policy.toml, not in the installation's policy/ folder, or korTTY was not restarted. The log's startup lines state which policy file (if any) was loaded |
| A rule does not apply to a user | Rule scoping is lowercase OS login names; check [groups] membership and remember that a more specific tier (user > group > everyone) overrides less specific rules |
| Warning "policy file is writable by the current user" | The installation directory permissions are too open — the enforcement model relies on admin-only write access |
| Admin model does not appear | See the log: GGUF URL downloads happen in the background at startup, and registration requires an installed llama.cpp runtime; MLX sources must be local safetensors directories |