All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
0.4.0 - 2026-09-01
- New
mcpobject inknosh.jsondeclares local MCP (Model Context Protocol) servers, modeled on OpenCode's local server schema:type(must be"local"),command(argv array) orcommandLine(a single string, tokenized POSIX-style),enabled(defaulttrue),environment, andtimeout(seconds). This is configuration and inspection only — servers are not yet launched or connected to any agent. See the configuration reference. - New
list-mcpcommand lists all servers configured undermcp, annotating disabled ones and showing an environment-variable count (never names or values). Seelist-mcp. - Agent frontmatter now supports an
mcp:block that overrides the globalenabledstate of MCP servers per agent, keyed by exact server handle or glob, with exact-beats-glob and last-matching-glob-wins precedence.list-mcp --agent <id>(or the resolveddefaultAgentId, when set) shows each server's global state alongside its effective state for that agent. Seemcpfrontmatter andlist-mcp --agent. - MCP servers are now actually launched: for each
prompt,kdocs, or file-defined command run, everyeffectiveEnabledMCP server is spawned over stdio and its MCP handshake completed before the run starts, and every process Knosh started is terminated when the run finishes (success or failure).mcp.timeout(seconds) now bounds spawn-plus-handshake, defaulting to 5 seconds when unset, and a newmcp.requiredfield (defaultfalse) controls what happens when a server fails to start: a warning on stderr and the run continues, or, whentrue, the run aborts. See the configuration reference. - Each MCP server's tools are now discovered during its startup window and merged into the agent's tool registry, offered to the model alongside Knosh's built-in tools. Each MCP tool is namespaced as
<serverHandle>_<toolName>(e.g.ksrc-mcp_search) so it can never collide with a built-in; Knosh built-ins always take precedence on a name collision, and any residual collision is dropped with a warning on stderr.AgentRunConfig.allowedToolIds(used today only bykdocs, to enforce its read-only tool set) filters the MCP registry by the same name rule as built-ins — since no MCP tool name can appear in that built-ins-only allowlist,kdocscontinues to offer no MCP tools. MCP tools have no per-tool permission model yet and cannot be named in an agent'stools:allowlist; the existing server-levelmcp:enable/disable in agent frontmatter remains the only per-agent gate over them. See the configuration reference,mcpfrontmatter, and the Tools reference. - MCP servers can now be
type: "remote", modeled on OpenCode's remote server schema: a requiredurland optionalheadersobject, connected over MCP Streamable HTTP (the deprecated HTTP+SSE transport is not supported, and OAuth is not supported — a server that requires it simply fails to connect and is handled by the existingrequiredpolicy). A remote server's tools are discovered and namespaced identically to a local server's, and the sameenabled/timeout/requiredfields and per-agentmcp:frontmatter overrides apply to both types. A header value exactly equal to{env:NAME}is resolved from theNAMEenvironment variable at connect time, so a secret like a bearer token never has to be written intoknosh.json; an unset variable is a startup failure handled by therequiredpolicy, same as any other.list-mcpprints a remote server'surland header count only — header names and values are never shown. See the configuration reference andlist-mcp. - New
add-mcpcommand writes a new entry undermcpinknosh.jsonwithout hand-editing the file, for both local (--commandLine/--command-line, or trailing argv after--) and remote (--url) servers. Server type is inferred, never declared:--urlmakes it remote, argv or--commandLinemakes it local. Also supports repeatable--env KEY=VALUE(local) and--header NAME=VALUE(remote),--timeoutSeconds/--timeout-seconds,--required,--disabled,--global(mirroringinit --global), and--forceto replace an existing entry — which is otherwise an error. The target file is created if it does not exist. On success,add-mcpprints the file it modified and echoes the JSON of the entry it wrote, wording a--forcereplacement (Replaced ...) differently from a fresh add (Added ...). Seeadd-mcp. - New
mcp-info <name>command prints one configured MCP server'slist-mcpdetails, then actually connects — spawns/connects, completes the handshake, discovers tools, and disconnects — to confirm the server is reachable and list what it exposes. It never executes a discovered tool. Unlike a real run, every startup failure is reported and the command exits1, rather than being swallowed as a stderr warning; a server that is effectively disabled (globally, or via a per-agentmcp:override resolved through--agent) is refused unless--forceis given. Each tool is shown under both its namespaced (<serverHandle>_<toolName>) and raw name;--timeoutSeconds/--timeout-secondsoverrides the server's startup budget for the probe, and--verboseadds each tool's required and optional parameters. A server that legitimately exposes zero tools is still a success. Seemcp-info. - Per-tool permission patterns can now be written relative to the working directory (e.g.
"src/*": "allow"), so one agent file works unchanged across every project it is used in. A pattern beginning*or?is used verbatim (keeping a bare"*"base rule matching every path); a pattern beginning/,~, or$HOMEis already anchored; every other pattern is resolved against the working directory.~/$HOMEare now also expanded in per-tool patterns. See Agent configuration.
- Closed a
..traversal bypass in per-tool permission pattern matching: a pattern was matched against the raw path string the model supplied, so a path likesrc/../secrets.txtcould satisfy anallowrule scoped tosrc/*even though it resolved outside that directory. Per-tool patterns and received paths are now both resolved and lexically normalized before matching, using the same primitive theexternal_directorycontainment check already uses.
- Consolidated the duplicated
AgentPromptExecutor/sinkFactory/interactivetest-seam defaults inknosh-cliinto shared, unit-tested factory functions, and extractedmain()'s error-handling and console-detection logic into an injectablerunKnosh/consoleAvailableseam, so this wiring is now exercised by tests instead of only by production use. RaisedkoverGatecoverage thresholds acrossknosh-cli,lib/knosh-agents, andlib/knosh-toolsto match the resulting measured coverage. No command-line behavior changed. - Config file merging now goes one level below the top-level key for
mcp,toolLimits, andwebFetch, instead of the local file's object replacing the global one wholesale.toolLimitsandwebFetchnow merge field by field.mcpmerges per server handle: a handle declared in only one file survives, a handle with a full definition in the local file replaces the global entry for that handle, and a local entry naming onlyenabled(e.g.{"enabled": false}) overlays the global entry instead — the supported way to disable or re-enable a globally-declared server from a project file. Anenabled-only entry with no counterpart to overlay in the other file is now a startup configuration error rather than silently doing nothing useful. See Config file merging. - Refreshed dependencies: Koog to
1.2.0(and its-betasiblings to1.2.0-beta), Kotlin to2.4.10, and smaller patch-level bumps to Ktor, Okio, Kover, kotlinx-io, and kmpLogger (to its2.0.0major release, which is behavior-preserving on the JVM). Two dependencies are deliberately held back:sort-dependenciesstays at0.19.0pending a fix for a known upstream regression, and the MCP Kotlin SDK stays at0.11.1because Koog'sagents-mcpstill pinskotlin-sdk-clientto that version.
0.3.0 - 2026-07-18
- A new
--benchmarkflag onprompt,kdocs, and file-defined commands prints a metrics summary to stderr after the run, showing token consumption, LLM call count, tool call count, and elapsed time. Example:42,318 in / 1,205 out tokens · 6 LLM calls · 3 tool calls · 12.4 seconds. The summary appears only when--benchmarkis set and stderr is an interactive terminal. For commands that run multiple iterations (e.g.,kdocsover several files), a single summary is printed at the end with token and call counts summed across all iterations. Token counts are omitted when the provider does not return usage metadata. - A live status line is shown on stderr while an agent runs in an interactive terminal, cycling a
Braille spinner through
starting…,calling model…, andrunning tool: <name>as the run progresses. It is omitted when iterations run in parallel (--parallelgreater than 1). - New
defaultAgentIdfield inknosh.jsonsets a project-wide fallback agent. When set,--agentIdbecomes optional forprompt,kdocs, and file-defined commands that do not declareagent:in their frontmatter. - New
toolLimitsobject inknosh.jsonmakes the maximum size of tool responses configurable:globMaxFiles(default 100),grepMaxMatches(default 100),textReadMaxBytes(default 51200), andwebFetchMaxBytes(default 131072). Each field name carries its unit of measure, and each value must be greater than zero. See the configuration reference. - New
toolLimits.grepMaxLineLength(default 100000 characters) andtoolLimits.grepTimeoutMillis(default 5000 ms) fields bound thegreptool's regex matching: lines are matched only within the firstgrepMaxLineLengthcharacters, and the whole search aborts aftergrepTimeoutMillisof wall-clock time, returning partial results with a note. See Limits. promptis now the default command. When the first argument is not a recognized subcommand name or a root-level flag (--help,--version), Knosh routes the invocation topromptautomatically. For example,knosh "summarize this"is equivalent toknosh prompt "summarize this", andknosh --agentId helper "hello"is equivalent toknosh prompt --agentId helper "hello". Runningknoshwith no arguments still prints root help.- New
webFetchobject inknosh.jsonconfigures the SSRF egress policy for theweb-fetchtool:allowedHosts(default[], meaning any host) restricts fetches to an explicit host allowlist, andallowInternalHosts(defaultfalse) controls whether a request may resolve to a loopback, link-local, site-local, any-local, multicast, or IPv6 unique-local address. See the configuration reference. - Knosh now warns on stderr at startup if the global config file (
~/.config/knosh/knosh.json), its parent directory, the local config file (.knosh.json), or its parent directory is writable by other users on the system — a world-writable config lets a local attacker rewrite trusted settings such as thewebFetchallowlist. The check is advisory only (Knosh does not refuse to start or alter permissions), flags only the world-writable bit (not group-writable), and is a silent no-op on non-POSIX filesystems. See File permission advisory.
- Hardened the
greptool against pathological caller-supplied regexes (ReDoS): a wall-clock time budget (toolLimits.grepTimeoutMillis) can now abort a match already in progress, and a per-line input-length cap (toolLimits.grepMaxLineLength) bounds the characters fed to the matcher. A search that exceeds its time budget returns partial results with a note rather than hanging or erroring. - Closed a path-traversal gap in filesystem-tool permission checks:
..segments (and un-expanded~/$HOME) in a tool's path argument could previously reach files outside the intended boundary, because the permission check and the actual file resolution derived different values from the same raw string. Path resolution and normalization are now unified behind a single primitive shared by both the permission check and the filesystem tools, and the external-directory boundary is now containment-based: a path that resolves inside the working directory is always allowed, and a path that resolves outside must match anexternal_directoryrule (see Agents). Symlink (real-path) canonicalization is a deferred follow-up — this fix normalizes lexically only. - Hardened the
web-fetchtool against SSRF: requests are now checked against a host allowlist (when configured) and, by default, refused if they resolve to a loopback, link-local (including the169.254.169.254cloud-metadata address), site-local, any-local, multicast, or IPv6 unique-local address. The tool now follows redirects manually instead of relying on automatic client redirects, re-checking every redirect hop against the same policy (capped at 5 hops), so an allowlisted URL cannot redirect its way into a disallowed internal address. Configurable via the newwebFetchobject; see Security / SSRF. DNS-rebinding / TOCTOU hardening is a documented, deferred limitation — not addressed by this fix.
- The
web-fetchtool now caps the returned content (after HTML-to-Markdown/text conversion) atwebFetchMaxBytesUTF-8 bytes — 128 KB (≈32K tokens) by default — instead of capping the raw download at 5 MB. A 5 MB internal limit still bounds how many raw bytes are read from the network.
0.2.0 - 2026-06-19
- File-defined commands, where you can wrap up a prompt with some YAML frontmatter and be able to readily reuse that prompt. See the documentation for more.
knosh updatewill install pre-packaged file-based commands, or update them with newer ones from the current installed copy of Knosh.- Proof-of-concept
review-planandreview-gradle-licensefile-defined commands are packaged in this version of Knosh. - New
commandsDirfield inknosh.jsonto relocate the commands directory. list-toolscommand to list all registered tools, their write-capability, and per-agent access rules
- Breaking:
knosh kdocs --parallelis now an integer-valued option (--parallel N) instead of a boolean flag. Valid range is[1, 32]; the default when omitted is1(sequential, equivalent to today's behavior when--parallelwas absent). Invocations that previously passed--parallelwithout a value will fail with a parse error and must be updated to--parallel N.
0.1.0 - 2026-05-25
init --globalnow creates an emptyagents/directory at~/.config/knosh/agents/in addition to writing the config fileinitcommand to write a defaultknosh.jsonconfig file, with--globalflag for writing to~/.config/knosh/knosh.jsonlist-agentscommand to list available agents in the configured agents directorykdocscommand to iterate over supplied source files and update their KDoc comments using an LLMpromptcommand (formerlyagent) to run a one-shot LLM prompt using a named agent- Agent configuration via Markdown files with YAML frontmatter (model, temperature, tool permissions, system prompt)
- Twelve built-in tools:
file-info,get-working-directory,glob,grep,list-directory-tree,text-read,web-fetch,create-directory,delete-file,move-content,text-edit,text-write - Support for multiple LLM providers via Koog: Anthropic, Mistral, OpenAI, and Ollama
- Per-agent and per-command temperature overrides via
--temperatureCLI option --max-iterationsoption to cap the number of LLM tool-call cycles per run- Retry policy for transient LLM and network errors, including Ollama-specific retries
- Tool-call logging with optional full-content logging via
--log-full-tool-calls --log-full-promptsoption to log full prompt content for debugging- Rich terminal output via Mordant (Markdown rendering in TTY; plain text elsewhere)
- Global and project-local
knosh.jsonconfig with optional per-provider API keys - Functional test suite running against a local Ollama instance