Skip to content

ngn: the terminal harness

ngn puts a terminal interface on top of the nagents library. The same coding harness powers interactive and headless use. Python extensions change the agent's behavior, not just its appearance.

The terminal and headless CLI are included in published distributions. This guide describes the current checkout, which may include newer fixes; it is not an OpenCode feature-parity claim. Read the limitations and trust model below before using it on important workspaces, and check release availability.

Related pages: installation and environment choices, complete configuration schema and recipes, and the local web client, available with the web extra and bundled assets.

Shared provider connections

Use /provider to create or edit named provider connections, activate one, or fetch its model IDs. The TUI and web UI share $XDG_CONFIG_HOME/ngn/providers.json (default ~/.config/ngn/providers.json). Enter an environment-variable name such as OPENAI_API_KEY or ${OPENAI_API_KEY}; ngn reads the value when it makes a request and does not save it in that file. OpenAI also supports ngn's ChatGPT device login or the library's local Codex discovery. Foundry can use the optional azure-identity SDK's DefaultAzureCredential chain. Live voice and backend settings belong to each connection. See the configuration reference.

Install and launch

From the repository root, retain the existing Poetry workflow:

poetry install -E dev -E tui
poetry run ngn --demo

Alternatively, for a new local uv environment:

uv venv --python 3.13
uv pip install --python .venv/bin/python -e '.[tui]'
uv run --no-project --python .venv/bin/python ngn --demo

Do not overwrite a Poetry-managed .venv. Use Poetry or another explicitly named local environment instead. The uv workflow needs neither uv sync nor changes to an existing uv.lock. The installation page explains activation and optional isolated tool installations. Commands written as bare ngn below assume the intended environment is activated; otherwise use poetry run ngn or uv run --no-project --python .venv/bin/python ngn.

Install the local source with the tui extra for the behavior documented here, or select a published CLI release as described in the installation guide. Textual is optional: importing nagents, using Agent, or running the headless client does not import Textual.

The offline demo needs no API key and does not execute shell commands, change workspace files, or contact a model. It also skips Python plugins, since even trusted setup code could perform I/O. It does save demo conversations locally. Send demo approval to inspect an approval dialog with a sample change preview. Demo actions are explicitly labeled as previews, not completed real work.

For a real model, create a named connection in providers.json or the Provider connections UI, set its referenced key in your shell environment, and launch:

poetry run ngn --provider-id my-openai --model gpt-6-luna
poetry run ngn --provider-id my-anthropic --model YOUR_MODEL_ID
poetry run ngn --workspace /path/to/project
poetry run ngn --continue

Use a model ID your provider account actually supports. Set base_url and api_key_env inside the named provider entry for custom endpoints; store key values outside configuration. Credentials are not copied from another coding assistant's configuration, and ngn does not automatically load .env.

Use api: auto for provider-native defaults, or choose chat_completions, responses, or messages for an endpoint supporting that route. LiteLLM requires an explicit base_url and an API-key reference such as api_key_env: LITELLM_API_KEY; it does not use ChatGPT OAuth or require a gateway SDK in ngn. See provider recipes.

Explicit text deliveries

The interactive TUI exposes the builtin.tui channel for explicitly requested text sends. Ordinary assistant replies already appear in the conversation. An approved channel_send appears as a separate saved delivery alongside its compact tool card; it does not create another assistant message or another execution loop. Deliveries survive session reload.

The route is text-only: attachments, including text files, cause the entire send to be rejected. Existing profile restrictions and terminal approvals apply. See built-in TUI text delivery for discovery, history and lifecycle behavior.

Composer and commands

Typing / at the start of the composer opens suggestions immediately. Use Up/Down or the mouse to select a command. Enter prefills its name; edit or add arguments, then press Enter again to execute. The menu, Ctrl+P palette, and /help all use the same live harness registry, including:

  • Built-in actions such as /new, /model, /agent, /tasks, and /queue.
  • Discovered skills as /<name> [task] or /skill:<name> [task], loaded on demand through the Agent's skill loader. Workspace reads remain guarded; bundled skills load from package resources. Suggestions refresh metadata without activating skills or running scripts.
  • Trusted Python-plugin commands, labeled with their plugin source.
Command Purpose
/help Show the live command registry.
/new Start a new local conversation.
/fork [title] In the TUI, copy the idle conversation into a new independent root and select it. Omit the title for <chat title> · fork 1, fork 2, etc. Saved context and attachments are copied; active work and approvals are not.
/rename <title> In the TUI, persist a name of 1–80 printable characters for the current conversation.
/sessions List and resume this workspace's saved conversations.
/compact Compact active context with the configured strategy.
/model [model] Show or change the runtime conversation model.
/agent [name] Show or switch the built-in/custom profile.
/context Inspect effective context and configuration.
/plugins Inspect configured and loaded Python extensions.
/skills [filter] Browse discovered skills and prefill a command without executing it.
/tasks Open the agent tree, inspect conversations, and send follow-ups.
/queue [resume\|clear] Inspect, resume, or explicitly discard queued input.
/login, /logout Manage ngn's local OpenAI device login.
/quit Close the client and clean up managed work.

/help is the source of truth for commands available in the particular source build you installed.

Shift+Enter inserts a newline. Ctrl+J and Alt+Enter remain fallbacks for terminals that do not transmit Shift+Enter distinctly. Pasted multiline text is never submitted automatically. Tab cycles agent profiles by default, and Shift+Tab cycles backward. Agent switching waits until the current run is finished or stopped.

New prompts and plugin/skill commands submitted during work queue in FIFO order by default. --submit-mode interrupt instead cancels and awaits the current run and its children before starting the new input. Ordinary configuration commands are not deferred behind an active run.

Escape stops current work and pauses the queue without discarding it. Empty Enter or /queue resume resumes queued input; /queue inspects it and /queue clear explicitly discards it. The queue is bounded to 20 inputs and is not persisted across application exit. Completed file or shell actions are never rolled back.

ngn --submit-mode queue --tab-action agent
ngn --submit-mode interrupt --tab-action complete
ngn --tab-action focus

The equivalent JSON settings are "submit_mode": "queue" and "tab_action": "agent". Tab alternatives are complete for command completion and focus for ordinary widget navigation. They also accept NGN_SUBMIT_MODE and NGN_TAB_ACTION environment defaults.

/resume is a synonym for /sessions and opens the session picker. Aliases are resolved by the command registry before native dispatch, so they appear in completion and help without being sent to the model. Extensions can register synonyms with harness.commands.register_alias(alias, target).

Selection and clipboard

Drag to highlight text and release the mouse button to copy it automatically. This works for responses, tool output, code, and mouse selections in text fields; code indentation and line breaks are preserved. Double-click selections copy too. Choosing an agent or slash-command row is navigation, not text copying.

Ctrl+C copies selected text instead of cancelling work. Without selected text, it retains the normal cancel/exit behavior. Ctrl+Shift+C explicitly copies without cancelling; Command+C also works if the terminal forwards it to ngn. Keyboard selection in an editable field does not automatically replace the clipboard, so select-all followed by paste still works normally. Use Ctrl+C to copy that selection.

ngn sends the terminal's OSC 52 clipboard request and also uses a local clipboard helper when available: pbcopy on macOS, wl-copy on Wayland, xclip or xsel on X11, or PowerShell on Windows. This makes copying work in macOS Terminal, which does not support Textual's OSC 52-only clipboard path. Helpers receive text on stdin, not as a shell command or command-line argument. ngn never reads your desktop clipboard automatically.

Over SSH, only terminal clipboard forwarding is used, not the remote desktop's clipboard. OSC 52 writing must be allowed by your terminal and, if applicable, tmux. If the terminal blocks it, configure its clipboard permissions or use the terminal's own selection/copy mechanism. Usual terminal paste shortcuts continue to work; pasted multiline text is never submitted automatically.

Compact tool activity

Tool cards start collapsed, including restored history and failed tools. Their single-line header shows the tool name, a short command or target hint when available, and a running, complete, failed, or cancelled label. Completed calls show their reported duration when available. Long names and hints are shortened to keep the outcome visible on narrow terminals.

Native shell results with a nonzero exit code or timeout show exit 7 (with the actual code) or timed out in the card and headless text output. This process outcome does not change the enclosing task's status or CLI exit code; the agent may recover and finish the requested work. Saved result strings are not parsed to infer a process outcome, and JSON events retain their original structured result and framework error fields.

Press F6 to switch from the composer to the conversation without changing your scroll position, or back to your draft. In the conversation, PageUp/PageDown scroll and Tab/Shift+Tab navigate tool headers and other controls. Tab in the composer retains its configured agent/completion/focus behavior. The conversation border and /help show the pane-focus shortcut.

Click a tool header, or focus it and press Enter or Space, to inspect the tool name, arguments, output, error, and diff. Press Enter, Space, or click again to collapse. Output and lifecycle updates never change your choice; failures stay visibly labeled without opening their details. While an expanded card's header is focused and visible, incoming details pause bottom-follow so the header stays available. Scrolling away does not pull the header back into view. Expansion choices last for the mounted card, not across session reloads. Details retain the existing 16,000-character output/argument preview limit with an explicit truncation marker; approval dialogs still show the exact proposal separately. In approval dialogs, PageUp/PageDown scroll the full proposal even with Deny or Allow once focused; paging does not change the decision focus. Deny remains the initial choice, and Escape denies. Closing an approval restores the previously focused control without scrolling; if that control is no longer available, focus returns to the composer.

Themes and motion

The message editor and its status footer share one borderless, filled surface. User messages use the same background, with paired foreground colors for contrast even when the surrounding terminal uses its native background.

ngn --theme terminal
ngn --theme graphite
ngn --theme ocean
ngn --theme ember
ngn --theme terminal --no-animations

terminal is the default. It uses ANSI default foreground/background resets, so your terminal supplies those colors; it does not sample RGB values or rewrite your terminal's palette. graphite is cool neutral, ocean uses blue/cyan, and ember provides the warm palette. Graphite combines lilac/mint/rose, ocean uses turquoise/blue/orchid, and ember uses terracotta/gold/mauve. User and assistant turns, tools, task states, notices, headings, code, and diffs have distinct semantic colors instead of sharing one accent. theme_background separately selects "auto", "terminal", or "theme": follow the preset's automatic behavior, retain the terminal background, or use the theme background. For example:

{"theme": "ocean", "theme_background": "terminal", "animations": false}

The equivalent CLI choices make background variants easy to compare:

ngn --theme ocean --theme-background theme
ngn --theme ocean --theme-background terminal
ngn --theme graphite --theme-background theme
ngn --theme ember --theme-background terminal

Native backgrounds use terminal defaults rather than attempting to sample the terminal's actual RGB color. Painted semantic text is tested for 4.5:1 contrast; native ANSI accent contrast depends on the terminal's own palette. Native selection uses reverse video, so arrow-key navigation remains visible without guessing whether the inherited background is light or dark.

Each preset has a subtle three-column ASCII activity animation at eight frames per second; idle/error status states remain still. In the current checkout, --no-animations, "animations": false in JSON, or NGN_ANIMATIONS=false disables the indicator, animated scrolling, and input cursor blinking. TEXTUAL_ANIMATIONS=none also disables these motions. Brief button-press feedback remains. Theme selection also accepts JSON theme and NGN_THEME; background selection has the NGN_THEME_BACKGROUND environment default. JSON overrides NGN_* environment defaults. These are color/motion presets, not a graphical Bot avatar or a separate team-view implementation.

Native background subagents

The agent can call delegate(prompt, agent="assistant"); assistant is the only built-in agent. Other delegation targets must be explicitly configured. The caller immediately receives a UUID task ID, a random two-word name such as quiet maple, and running status. The child runs as a native asyncio task while the parent continues its own work.

When the parent turn settles, completed results are batched into an explicitly untrusted background notification, and the parent automatically runs again to consider them. If children are still working, the harness waits for their results without holding a model request open. The UI shows each child's task and response; /tasks opens the current-session conversation inspector. Headless JSON includes task_started, task_message, task_completed, task_notification, and task_activity events, with a final done only after the group settles. Task lifecycle events include parent/child session IDs, parent task ID, depth, profile, follow-up number, activation number, and trigger. Notification records identify the actual source and recipient; they do not themselves assert that another execution completed.

task_activity wraps a child's published messages and tool evidence in a separate payload with the task, parent, child session, activation, turn, sequence, and timestamp. It is inspection data, not a main-assistant answer or root tool call. Consumers must keep that scope when rendering concurrent children. Private reasoning and provider request metadata are not included. Live voice and channel output receive the parent's response rather than this inspection stream.

Queued completion events reach the UI before the next core model or tool-result event, even while the parent keeps working. Waiting for a tool result or a model event can delay that update; streamed shell output alone does not trigger it. Model notifications still wait for the parent turn boundary. If a parent hits a terminal error, already queued lifecycle events are emitted before cleanup, but no new model turn starts. A child that recovers from a recoverable provider error and finishes successfully returns its result normally.

Nested completion notifications target the immediate parent. Main can observe all task lifecycle events without receiving every grandchild result as a separate model instruction. When an eligible retained child is continued after its parent has finished, that parent is reactivated in its own conversation, synthesizes the update, and reports upward. Cancelled or unavailable ancestors are not silently bypassed. Automatic activations have their own activation number and trigger; they do not count as human follow-ups or reset the root execution budget.

This preserves provider protocol rules: the delegation call has exactly one matching tool result (the started acknowledgement). Late completion is not a second tool result and is not promoted to a system instruction. It is a new, labeled user-role data notification at a safe turn boundary, using the same Agent.run, hooks, context handling, and permissions as ordinary turns.

The built-in assistant uses normal guarded operation. Custom agents can select read-only mode through their profile configuration. Children have the same permission ceiling as their parent, not independent authority. A read-only parent cannot obtain write or shell privileges by delegating to assistant or a build-mode custom profile. Build-capable child actions still need the ordinary guarded-tool approvals. An instruction or profile name does not bypass this boundary.

max_subagent_depth defaults to 2 and accepts integers from 0 through 8. Depth counts the root as 0, a child as 1, and a grandchild as 2. The default therefore permits grandchildren, but not great-grandchildren. Set it to 0 to disable delegation, or 1 to allow only direct children.

Depth is separate from concurrency and job budgets. Additional initial bounds are three concurrent subagents and eight child executions per root user run, shared across the entire descendant tree and including automatic continuations and accepted human follow-ups. A quota-full delegation fails immediately instead of waiting indefinitely for a slot; a parent still working or waiting for its own children occupies a slot. Further limits are at most min(max_tool_rounds, 12) tool rounds per child, a five-minute child timeout, a 16,000-character delegation prompt, and 12,000-character results. These are implementation limits, not extra YAML knobs. Children use separate sessions/providers and share the parent's credential manager for in-process refresh coordination. They receive the delegated prompt and applicable instruction context, not a copy of the full parent conversation. Plugins are not re-imported in children. Children inherit the active model and provider unless their profile or delegation supplies an explicit selection. The delegate tool accepts model, a configured provider connection, and compact_trigger, compact_tokens, and compact_messages choices. Compiled Agent Designer definitions retain control of their provider and model.

Tasks are process-local, not durable jobs or persistent Bot identities. Cancelling or closing a run cancels and awaits its children; undelivered notifications are discarded, and task outcomes are not replayed after restart. Child transcripts remain in local storage but do not clutter the ordinary session picker.

The web client additionally owns a process-local timer service for schedule_wakeup. It resumes the scheduling root or child without keeping a browser request open. Ordinary CLI/TUI instances do not own that service and reject timer requests explicitly. Timers do not survive a server or pod restart.

Agent tree and follow-ups

The right rail contains a scrollable tree of running and finished agents. Nested delegations appear under their parent, with status labels. Collapse branches with Left/Right or the mouse; selection and expansion survive status updates. Ctrl+T focuses the tree, and Enter or a click opens the selected conversation. On narrow terminals, Ctrl+T opens the same tree inside the conversation inspector instead of squeezing the main conversation. /tasks opens the inspector at any width.

The inspector shows saved user/assistant/tool messages and a per-agent follow-up draft. Use Up/Down to select, Left/Right to expand or collapse, and Tab or Enter to move into the draft. Ctrl+Enter or Continue Agent sends it. Selecting a node never sends a message. You can inspect and draft while work runs, but must wait for that child and its subtree to settle before continuing it. Cancelled tasks cannot be resumed; delegate a new task instead.

A follow-up continues the same child conversation and identity, not a new unrelated agent. Its message stays in that child's transcript; the parent receives a pickup notification and the child's returned result. If the parent is already running, it receives the notification at its next safe turn boundary. If idle, ngn waits for the child result and runs the parent to consider the exchange. It does not inject a second tool result or silently treat child output as higher-priority instructions.

Each identity permits eight explicit human follow-ups, and the root retains up to 64 identities for its process lifetime. Every continuation also consumes one of the current root run's eight execution slots. Full quotas and unsupported continuations are rejected explicitly. These handles do not survive restarting ngn, even though the underlying transcripts remain on disk.

The web task inspector also accepts messages while a child is running. These wait for its next complete response and tool batch, including any pending approval. Reading a child conversation never starts it or approves its tools. The web inspector preserves readable task records after restart; the workers themselves remain process-local.

Python clients use the same interfaces:

from contextlib import aclosing

tasks = harness.tasks.list()  # Detached snapshots, including descendants.
messages = await harness.task_history(tasks[0].id, limit=100)
async with aclosing(harness.continue_task(tasks[0].id, "Check this case as well.")) as events:
    async for event in events:
        handle(event)

The history limit is 1..200; inspection does not run a model or repair an unfinished tool block. During an existing root run, continue_task() yields an acceptance notice; task events keep flowing through that run's existing consumer. When idle, the returned stream owns execution and must be consumed or explicitly closed, just like Harness.run().

Try demo subagents in ngn --demo for three genuine concurrent asyncio jobs using clearly labeled, scripted offline responses. A live example is:

Delegate three independent read-only reviews of the README, test setup, and package configuration. Continue your own review, then combine the results.

Protocol references: OpenAI function-call correlation and Anthropic tool-result ordering.

Provider sign-in

ngn login chooses the provider and stores one active login. In a terminal it shows a menu; name the method to skip it:

ngn login                    # interactive menu
ngn login chatgpt            # ChatGPT/Codex device code (same as --device-auth)
ngn login openrouter         # browser PKCE; paste the code OpenRouter shows
ngn login openai             # hidden API-key prompt
ngn login anthropic --model MODEL-ID
ngn login gemini --model MODEL-ID
ngn login custom --base-url https://gateway.example/v1 --model local-model
ngn login --status
ngn logout
Method Provider Notes
chatgpt openai ChatGPT/Codex device login; subscription access, not general API access.
openrouter openrouter Headless PKCE: open the printed link, approve ngn, and paste the single-use code shown by OpenRouter (valid for 10 minutes). ngn exchanges it for a user-controlled API key.
openai openai OpenAI Platform API key with usage-based billing. Default model gpt-6-astra.
anthropic anthropic Anthropic API key; enter a model ID when prompted.
gemini gemini Google AI Studio API key; enter a model ID when prompted.
custom openai_compatible Your own gateway, Ollama, or another compatible service; requires a base URL and model. --api selects the HTTP contract, and --provider accepts any advanced provider name.

API keys are read with a hidden prompt, or from standard input with --key-stdin, never from the command line. Passing --api-key-env NAME stores only a reference to an environment variable instead of a key, which suits a secret manager. A non-interactive ngn login without a method keeps the historical ChatGPT device-code behavior.

Saved login credentials do not select an active connection. Pick a named connection in providers.json or with --provider-id; ChatGPT device login supplies credentials when that connection uses "auth": "chatgpt". ngn login --status prints saved credential status without a network request.

The active login lives in $XDG_DATA_HOME/ngn/auth/login.json (~/.local/share/ngn/auth/login.json by default), a plaintext credential file protected by POSIX permissions (directory 700, file 600) next to the ChatGPT store. Only one login is active: signing in to another provider replaces the previous selection and key. Keys are never printed, logged, added to model context, or rendered by status output. /logout or ngn logout removes ngn's saved provider login and ChatGPT credentials; it does not touch other clients or revoke remote sessions. Built-in file tools deny the entire credential directory, and sign-in is disabled in offline demo mode.

OpenRouter browser sign-in

ngn login openrouter uses OpenRouter's headless PKCE flow: ngn prints an openrouter.ai/auth address with a generated code challenge, you approve the request in your browser, and OpenRouter displays a single-use authorization code. Paste that code at the prompt. ngn exchanges it over HTTPS for a user-controlled API key and stores it like any other API login; the code cannot be reused and expires after 10 minutes. Only continue if you started the sign-in yourself. The default model is openrouter/auto; change it with --model or /model.

OpenAI device login

Start ngn without --demo, then enter /login. Select ChatGPT device login, open the displayed OpenAI link, and enter the one-time code in your browser. Only approve a code you requested yourself. Escape or Cancel stops waiting. The terminal never asks for your ChatGPT password.

You can also use the headless login command:

ngn login --device-auth
ngn login --status
ngn

OpenAI currently documents device authentication as beta. You may need to enable it in your ChatGPT security or workspace settings. Your account/workspace must have Codex access. If authorization is unavailable, use an OpenAI Platform API key instead; device login does not bypass account restrictions or turn a ChatGPT subscription into general API access.

ChatGPT sign-in uses https://chatgpt.com/backend-api/codex/responses, not the ordinary API-key chat-completions endpoint. Choose a model available to your account in config.json, /model, or --model. The underlying OpenAIProvider lives in the library; interactive login, token refresh/storage, and route selection live in the harness.

The auth field in the selected providers.json connection accepts:

  • auto: prefer a saved ChatGPT login for the default OpenAI endpoint with api: auto; otherwise use the named key environment variable.
  • api-key: always use the configured API-key environment variable.
  • chatgpt: require the saved ChatGPT login and the Codex Responses route.
  • codex: use local Codex discovery rather than the ngn device login.

Saved ChatGPT credentials are never sent to a custom base_url. Such endpoints use API-key authentication, and explicitly combining auth: chatgpt with a custom endpoint or an api override is rejected. Keep api: auto for ChatGPT/Codex login, even though its dedicated route uses Responses. Device login is disabled in offline demo mode.

Tokens are stored in $XDG_DATA_HOME/ngn/auth/openai.json, or ~/.local/share/ngn/auth/openai.json by default. This is a plaintext credential file, protected by restrictive file/directory permissions on POSIX, not an encrypted keyring. It is separate from the workspace and session database. Refresh tokens rotate during use. Treat the entire file like a password: never commit, attach, or paste it into bug reports. Built-in file tools deny access to this store even when the workspace is your home directory.

/logout or ngn logout removes ngn's local saved login. It does not touch other clients' credentials or revoke remote sessions. ngn never imports .codex or OpenCode credential caches. Device codes are displayed only by the login UI or explicit login command; they are not added to model context or conversation history.

Headless use

poetry run ngn run "Explain how tests are organized"
poetry run ngn run --json "Review the current implementation"
poetry run ngn run --demo --json "Show the interface event stream"
poetry run ngn sessions
poetry run ngn doctor

The same commands work through the local uv launcher, for example:

uv run --no-project --python .venv/bin/python ngn run --demo --json "Hello"

ngn run - reads a prompt from standard input. JSON mode emits one event per line with schema_version: 1 and an event discriminator. It uses the same tools, hooks, and permission checks as the TUI. Non-interactive approvals are denied, never silently granted. An interactive ngn run asks for approval on standard error. Control sequences in human-readable tool output are sanitized.

Before the prompt runs, ngn run prints a short header on standard error with the effective provider, the effective model (a profile's model can supersede the top-level model), and the configuration files that were actually loaded:

Provider: openai
Model: gpt-6-luna
Config: /home/you/.config/ngn/config.json

When no configuration file exists the header reads Config: built-in defaults, and --demo adds a Mode: offline demo line. The header always goes to standard error, so ngn run --json keeps standard output as pure JSON Lines. ngn doctor lists the same files as Config files:.

--json is a run option, not an option for every subcommand. Human-readable text streams to stdout; tool progress and approval prompts use stderr. JSON mode writes versioned event records to stdout, with command errors/denials still possible on stderr. Consumers should dispatch on event, tolerate additional fields, and handle tool failures as well as the final response. Subagent events include task_started, task_completed, and scoped task_activity records.

Exit codes are 0 for a completed command, 1 for a run that emits an agent error, 2 for a configuration/usage failure, and 130 for interruption. A completed agent run can contain a denied or failed tool that the model handled; inspect tool-result events when automating a workflow.

CLI reference

Run without a subcommand for the TUI. Common options are accepted before or after the subcommand; prefer supplying each option only once.

Command Purpose
ngn Full-screen terminal client. Requires the tui extra and a terminal.
ngn run [--json] [prompt ...] Headless prompt execution. -, or no prompt with piped stdin, reads stdin.
ngn serve React client and HTTP API for the harness. Install [web] with bundled assets, or build an editable checkout; see Local Web Client.
ngn sessions List saved sessions for the selected workspace.
ngn resume [SESSION_ID] Resume a saved session in the full-screen client. With no ID, choose from an interactive list. Requires a terminal; errors on an ID from another workspace.
ngn doctor Initialize the harness and show configuration/extension diagnostics without an LLM request. Trusted plugins still execute. Use --demo to skip them.
ngn login [METHOD] Start provider sign-in. Methods: chatgpt, openrouter, openai, anthropic, gemini, custom, or an advanced --provider name. Interactive menu in a terminal; ChatGPT device login when none is chosen non-interactively.
ngn login --status Show ChatGPT and provider login status without a network request.
ngn logout Remove ngn's saved ChatGPT and provider logins.
ngn --version Print CLI/package version; the version alone does not prove an unreleased feature is present.
ngn --help, ngn run --help Show options implemented by the installed source build.
Common option Meaning
--workspace PATH, -C PATH Select an existing workspace directory.
--config PATH Load and explicitly trust a JSON file at any location.
--trust-project Also trust <workspace>/.ngn/config.json.
--provider-id NAME Select a named connection in providers.json.
--model ID, -m ID Set top-level model selection. A profile's own model can supersede it on activation.
--agent NAME, -a NAME Select the built-in assistant or an explicitly configured custom profile.
--max-subagent-depth N Integer 0..8; default 2, with the root at depth 0.
--theme NAME Select terminal, graphite, ocean, or ember.
--theme-background auto\|terminal\|theme Choose preset-default, terminal-native, or preset background behavior.
--no-animations Disable the busy indicator's motion, animated scrolling, and cursor blinking; brief button feedback remains.
--submit-mode queue\|interrupt Control prompts submitted while work is active.
--tab-action agent\|complete\|focus Select composer Tab behavior.
--plugin REFERENCE Explicitly trust and append a Python extension; repeatable.
--demo Use the offline demo.
--continue, -c Resume the latest session in this workspace, if any.
--resume ID Resume a specific session in this workspace. Mutually exclusive with --continue.

Connection authentication, API, endpoint, and key-variable name belong in providers.json; config.json holds general settings such as data_dir, shell_timeout, limits, model selection, and profiles. Use the configuration reference for types and validation, and --help for the flags present in the particular source build.

Session storage

Sessions are scoped to the resolved workspace path. The default state root is $XDG_DATA_HOME/ngn, falling back to ~/.local/share/ngn; each workspace uses a hashed subdirectory containing sessions.db. Set data_dir to change the session-state root. Moving a workspace or changing its resolved path can select a different scope; --resume does not search unrelated workspaces.

ngn resume opens the full-screen client on a saved session. Pass a session ID to resume it directly, or omit the ID to pick from an interactive list. Listing and choosing are read-only: they never register an empty session, so the picker shows only real conversations. --continue/--resume on ngn and ngn run remain available for scripts and one-shot runs.

Demo conversations persist too. Queued prompts and running subagent jobs do not survive application exit. Resuming history does not replay completed or uncertain tool actions, and child transcripts are not ordinary session-picker entries. Session databases can contain private prompts and workspace content; do not commit or attach them to bug reports. The OpenAI credential store is separate and is not relocated by data_dir.

Configuration

User configuration is ~/.config/ngn/config.json (or under $XDG_CONFIG_HOME). Project configuration is <workspace>/.ngn/config.json and requires --trust-project or explicit selection of that file with --config. --config /any/path/settings.json explicitly trusts that file; its name and location need not be .ngn/config.json. Settings are top-level JSON with no ngn wrapper; named profiles live under a profiles object:

{
  "providers": "./providers.json",
  "model": "gpt-6-luna",
  "agent": "assistant",
  "shell_timeout": 60.0,
  "max_subagent_depth": 2,
  "profiles": {
    "audit": {
      "mode": "reviewer",
      "instructions": "Prioritize regressions and missing tests."
    }
  }
}

Precedence is built-in defaults, NGN_* environment defaults, user JSON, trusted project JSON, explicit JSON, then CLI flags. NGN_MODEL and NGN_PROVIDER_ID are examples of environment defaults. Plugin paths in JSON resolve relative to that file; explicit --plugin paths resolve relative to the shell's current directory. data_dir in JSON is also relative to that file, not the workspace. JSON plugin lists replace earlier lists; CLI --plugin entries append. Profiles merge by name, but a later same-name entry replaces the whole profile, with omitted fields resetting to profile defaults. No literal API keys belong in JSON.

See the full configuration reference for every accepted field, exact types/defaults/ranges, provider recipes, and the distinction between configuration precedence and runtime profile model selection.

Default and workspace instructions

Every coding Harness starts with ngn's bundled nagents/harness/instructions/AGENTS.md. The same Markdown resource ships in the wheel and source distribution; it contains the default coding, verification, and approval guidance. Framework maintainers can edit src/nagents/harness/instructions/AGENTS.md to change those defaults without editing Python prompt strings. Active workspace, profile, tool capabilities, and delegation limits are added by the runtime.

For project-specific guidance, create or edit your workspace's AGENTS.md. The Harness loads an existing root file automatically when initialized, including when constructed programmatically; it never creates or overwrites this file. Native file tools also load applicable nested AGENTS.md files when accessing their paths. Nested guidance takes precedence within its subtree. These files provide task context and cannot grant tools, bypass approvals, or change project configuration trust. Ordinary delegated children receive the defaults once and retain the applicable workspace guidance.

Agent Designer definitions keep their explicitly configured instructions and do not implicitly load either coding-harness defaults or workspace AGENTS.md.

Skills

The Harness includes ngn-customize for the application's customization interfaces and ngn-channels for external messaging integrations. Add workspace skills as .ngn/skills/<name>/SKILL.md or .agents/skills/<name>/SKILL.md with single-line name and description frontmatter. Workspace names take precedence over bundled names.

The model sees escaped descriptive metadata in a separate system-level message and calls skill(name) to load relevant instructions as tool-result task context. The preview has a separate 10,000-estimated-token cap, complete sorted entries, and an omission notice; omitted names remain in the full catalog for named loads. Explicitly select one as user-level task context with $ngn-customize or $ngn-channels, including ordinary web/Telegram messages. The web UI and TUI also provide /skills [filter], /ngn-customize [task], and /skill:ngn-customize [task]. Short names yield to built-in or registered commands; the skill: form resolves collisions. Arguments retain their whitespace, replace literal $ARGUMENTS in the skill, or append as task text when no placeholder exists. Existing load budgets and tool permissions still apply. Dollar mentions and slash commands are ngn/client conventions, not universal Agent Skills syntax.

The catalog refreshes at incoming text boundaries, before each model request, and before/after every tool invocation. Changes therefore become available during the same turn without restarting ngn. Each load defaults to a 10,000-token approximation based on UTF-8 bytes divided by four, with UTF-8-safe truncation and a bounded aggregate allowance for explicit activation. Trusted YAML skill_token_limit = 10000 or the NGN_SKILL_TOKEN_LIMIT environment default configures this same allowance (integer 1–100,000). All discovery sources share the top-level single-line metadata parser; extra and nested metadata is ignored. Loading adds task context and executes no scripts; referenced resources require separate permitted access. See Skills for authoring, library discoverers, loading results, and batch/realtime limitations.

Python behavior extensions

Reloadable setup contract

The coding harness stages configured extension setup on initialization, at user message boundaries, and unconditionally after each model response. Register tools with harness.agent.register_tool or harness.agent.tool_registry, commands and aliases with harness.commands, and return an AgentPlugin (or append one to harness.agent.plugins). Setup may replace max_tool_rounds, streaming, system_prompt, or compaction_strategy; these model behavior settings revert when their extension is removed. workspace and a configuration snapshot are available while staging. Setup must not replace host-owned providers, executors, sessions, tasks, or approval handlers. An unsupported mutation rejects the entire candidate and retains the last working tools, commands and MCP connections.

Retained tool closures forward to the live harness after setup, so they see the current session and approval policy. Source files and package-relative imports are compiled in private generation namespaces, including same-size edits made within one filesystem timestamp tick. Unrelated absolute imports retain normal Python ownership; host and framework modules are never evicted. Put owned resource cleanup in the async AgentPlugin.aclose method; import/setup failures must clean up resources acquired before a hook is registered. Trusted Python still has full process privileges; this registration contract is not a sandbox.

A generation’s hooks remain attached to the response and calls that used it. after_run cleans up that generation’s participation before retirement, and aclose releases it. A replacement hook initializes with before_run before its first model request in the continuing conversation; that return value does not rewrite the existing user message. Use before_model for new request context. Host-installed library hooks retain the normal once-per-run lifecycle below.

Register slash commands

Trusted setup functions can register a prompt template or an async handler:

from nagents.harness import CommandResult


async def status(harness, arguments):
    return CommandResult(message=f"Current model: {harness.agent.provider.model}")


def setup(harness):
    harness.commands.register(
        "review-code",
        "Review a selected file",
        prompt="Review this file without editing it: $ARGUMENTS",
        argument_hint="<file>",
        requires_arguments=True,
    )
    harness.commands.register("local-status", "Show local status", handler=status)

Prompt templates replace only literal $ARGUMENTS; JSON/Python braces are not interpreted. A CommandResult contains either a local message or a prompt for the normal agent loop. Discovery does not execute handlers. Built-in names and the skill: namespace are reserved, and duplicate names are rejected. Plugin attribution is automatic while setup runs. Async handlers retain full, trusted Python access; they are not sandboxed or automatically mediated as tools.

See examples/harness/commands.py in the checkout, or launch it with ngn --plugin examples/harness/commands.py:setup and type /.

Change agent behavior

The supported interfaces live in nagents.extensions and are also exported from nagents. They do not depend on the coding harness or TUI.

Interface Purpose
AgentPlugin.before_run Transform an incoming message
AgentPlugin.before_model Transform request-local messages, tools, and generation configuration
AgentPlugin.after_model Observe each completed response, including final text, before calls execute
AgentPlugin.after_tools Observe the completed tool batch before building the next request
AgentPlugin.aclose Release a retired harness extension generation’s resources
AgentPlugin.before_tool Transform tool arguments before the execution permission boundary
AgentPlugin.after_tool Transform the tool result
AgentPlugin.on_event Observe core events
AgentPlugin.after_run Run lifecycle cleanup
CompactionStrategy.should_compact Decide when custom compaction should run
CompactionStrategy.compact Replace the active message context using arbitrary Python logic

Hooks run in plugin order. A plugin receives a RunContext containing the agent, session, user, and current round. This gives trusted code access to the underlying library rather than a second, restricted imitation of its API.

The plugin chain is snapshotted at the start of a run. Event observers receive copies; use transformation hooks to change behavior. Tool hooks must preserve the call ID and name. Hook failures stop the run, and cleanup attempts every plugin's after_run hook. These extension contracts currently apply to the text agent loop; voice, batch, and run_simple() reject extension-enabled execution rather than silently bypassing a permission boundary.

Request transformations and persistent compaction are different operations. before_model affects the next request; it does not append its transformed messages to the transcript. A compaction strategy returns a CompactionResult containing replacement active messages. Those messages are stored atomically and the active-history boundary advances without deleting older rows. Replacement histories must keep tool calls and results paired.

The strategy receives active history without the configured system prompt. It can trim complete turns, call its own summarizer, retain task state, retrieve memory, or implement another algorithm. Manual compaction uses the same strategy. The default ModelCompaction strategy summarizes history using the selected provider. Configure its thresholds, prompt, or summarizer provider directly; set compaction_strategy=None to disable compaction.

See the runnable example:

poetry run ngn --plugin examples/harness/custom_behavior.py:setup

That extension installs a no-LLM recent-turn compactor, adds request-local verification instructions, and registers a Python tool. It deliberately discards old active context rather than summarizing it, making the distinction between algorithm replacement and prompt customization easy to inspect.

A harness extension exports setup(harness), synchronously or asynchronously. It may configure harness.agent and return an AgentPlugin. Existing library features remain accessible from this trusted Python entry point; for example, an application can compose MCP clients and register their tools without adding MCP implementation details to the terminal widgets.

Trust and permissions

  • Global configuration and an explicitly selected --config file are trusted.
  • Project .ngn/config.json is ignored unless --trust-project is supplied or that file is explicitly selected with --config. Review it and referenced Python code before enabling it.
  • --plugin path.py:setup explicitly trusts that Python code. Installed-module entry points use the same module:setup notation.
  • Plugins have the process's full privileges. They are not sandboxed, and may use the network or write files during setup. Only load code you trust.
  • Built-in file tools restrict paths to the workspace, check symlinks, and guard sensitive paths. Changes require a preview and approval, with conflict checking to avoid replacing edits made while an approval is open.
  • Shell execution is local, not sandboxed. An approved shell command can access anything your user account can access, including outside the workspace.
  • The reviewer profile denies modifying tools and shell execution. Skills and system prompts are not the mechanism enforcing those restrictions.
  • Skills provide task guidance on demand through the Agent loader. Workspace resources retain file-tool restrictions; bundled bodies use package resources. Loading grants no permissions and does not execute scripts or automatically read referenced files.
  • Interruption stops managed work but never rolls back already completed actions. An uncertain tool outcome is not automatically replayed on resume.

No automatic Git commit, reset, checkout, push, or rollback is performed. Review the diff and run appropriate checks before accepting generated changes.

Customer test loop

  1. Start with ngn --demo; try streaming, demo approval, denial, cancellation, a new session, and session resume.
  2. Resize to a small terminal and confirm the composer and approval controls remain usable. Exercise multiline input and command discovery.
  3. Configure a real provider and ask for a read-only explanation of a disposable project. Check which tools and context were used.
  4. Ask for a small edit. Inspect the exact preview before approving, then review the resulting Git diff and run a relevant test.
  5. Load examples/harness/custom_behavior.py:setup and inspect the active extension/compaction behavior. Try your own context transform or compactor.
  6. Report the command, terminal size/emulator, provider/model, expected behavior, and the observed failure. Do not include API keys or secret file contents.

Scope

The first version focuses on a local coordinating agent with bounded subagents, Python behavior extensions, coding tools, sessions, and a terminal/headless interface. It does not yet offer remote attachment, persistent peer-agent groups, worktree isolation, LSP integration, extension marketplaces, cross-process exactly-once tool execution, or a secure sandbox for arbitrary plugins and shell commands. Realtime speech remains a separate library workflow. Guarded filesystem tools and process-group shell cleanup currently target POSIX; Windows is not yet a supported coding-tool host. Skills use a lightweight single-line name and description frontmatter subset; other Agent Skills metadata does not configure tool permissions. See the skills guide for the standard and host-specific choices. Ignore matching is a documented subset rather than complete Git wildmatch semantics.

For the reasoning behind these boundaries and the upstream comparison, see Harness extensibility research.