Skip to content

Neo CLI Reference

Canonical CLI documentation. Updated with every chip of the CLI-expansion initiative (plans/cli-expansion/_master-plan.md). Commands marked (legacy routing) still dispatch through the historical if-chain in bin/neo.ts and migrate onto the command framework chip by chip.

Invocation modes

FormBehaviour
neo [message]Interactive TUI (default). A message seeds the first turn.
neo -p, --print <message>One-shot: print the response and exit.
neo --jsonLine-delimited JSON control protocol on stdio (docs/agent-cli.md).

Global flags (framework commands)

FlagMeaning
--jsonMachine output. Success: { "ok": true, "data": … }. Failure: { "ok": false, "error": { "code", "message", "suggestion?" } } on stdout.
-q, --quietMinimal output (ids / single tokens only).
--verboseExtra diagnostic detail on stderr.
--no-colorDisable ANSI colors. The NO_COLOR env var is also respected.
-h, --helpHelp: neo --help, neo <group> --help, neo <group> <cmd> --help.
--versionPrint the CLI version.

Legacy-routed commands accept only the flags their own help lists; they gain the global set as they migrate.

Exit codes

CodeMeaning
0Success
1Generic / unexpected error
2Usage error (unknown flag, missing argument)
3Not found
4Unavailable (e.g. daemon not running)
5Authentication required
6Conflict / denied

Commands

System

neo status

Whole-system view: daemon health (HEALTHY/STALE/DEAD/FOREIGN), heartbeat age, capability tier, today's token budget, supervised channels, active provider/model, and log/audit paths. Reads the daemon's on-disk state — works whether or not the daemon is running.

$ neo status
Daemon:    HEALTHY
  pid:       4242  v0.1.0  @host
  heartbeat: 12s ago
  tier:      2
  tokens:    18500 spent today
Provider:  claude / claude-sonnet-5
Channels:
  CHANNEL   STATE  PID   RESTARTS  UPTIME
  telegram  up     4310  0         3600s
Log:       ~/.neo/daemon/daemon.log
Audit:     ~/.neo/daemon/audit.jsonl

$ neo status --quiet
healthy

$ neo status --json | jq .data.health
"dead"

For the current interactive session, use /status inside the TUI.

neo completion <bash|zsh|fish|powershell> — shell completion

Emit a tab-completion script. 100% generated from the live command registry (cliRegistry.list()) plus the still-legacy top-level commands bin/neo.ts routes by hand — there is no separate hand-maintained flag list to drift from the real commands.

# bash
neo completion bash >> ~/.bashrc
# zsh
neo completion zsh > "${fpath[1]}/_neo"
# fish
neo completion fish > ~/.config/fish/completions/neo.fish
# PowerShell
neo completion powershell >> $PROFILE

Completion depth is top-level command + one subcommand level. Once a recognized neo <command> [<subcommand>] is on the line, TAB also offers that command's own flags (e.g. neo daemon logs --<TAB>--tail --follow, plus the global set) for bash/zsh/powershell — sourced straight from each command's defineCommand({ flags }). Fish stays scoped to command+subcommand (no positional bookkeeping — fish's __fish_seen_subcommand_from only checks token presence, not position), which is enough to gate flags to the right subcommand without a third completion pass. A command not yet migrated onto the framework registry (see "legacy routing" callouts throughout this doc) has no flags to read from, so it falls back to just the global flags, same as before this existed.

Regenerate after upgrading — the script embeds the command/flag list known at generation time.

Config

Manage settings.json (dotted-path get/set/unset) and validate all three config files. providers.json / mcp.json mutation lives in other CLI- expansion chips — this group only writes settings.json, but validate reads all three since that's cheap and read-only.

Secrets guard: any dotted key matching /key|token|secret|password/i never prints its value — list/get render <redacted> instead, in both human and --json output. This applies even to nested values, e.g. webSearch.providers.tavily.apiKey.

The key pattern is deliberately broad (favors over-matching a false positive over ever missing a real secret), but redaction only fires when the value itself is a string — secrets are always strings, and a number or boolean can never hold a credential. So a key name that happens to contain "token" but holds a non-string value, e.g. maxTokensPerTurn, escalatedMaxTokens, maxTokenRecoveries (all numbers), prints its real value; a string-valued key matching the pattern, e.g. webSearch.providers.tavily.apiKey, still always redacts.

neo config list [--defaults]

List every resolved setting (settings.json merged with built-in defaults) as dotted key/value pairs. --defaults adds a column marking which values equal the built-in default (yes/no).

$ neo config list
KEY       VALUE
maxTurns  30
logLevel  info
...

$ neo config get daemon.webhook.port --json
{"ok":true,"data":{"key":"daemon.webhook.port","value":7777}}

neo config get <key>

Dotted-path lookup in the resolved settings, e.g. neo config get daemon.webhook.port. An unknown or unset key exits 3 (not found); when a close match exists among known keys, the error includes a "Did you mean…?" hint (stderr for human mode, error.suggestion for --json).

neo config set <key> <value>

Schema-validated write to settings.json, through the same durable write seam the cc-ported ConfigTool uses (runtime/config-loader.ts). The raw value parses as JSON when possible — true/false → boolean, 5 → number, [1,2] → array, {"a":1} → object — and falls back to a plain string otherwise. A value that fails config-schema.ts validation (e.g. microCompact, webSearch, channels sub-blocks) exits 2 (usage error) with the zod error message; the file is left untouched. On success the in-process config cache is reloaded immediately.

$ neo config set logLevel debug
logLevel: "info" -> "debug"

$ neo config set webSearch.maxResults -5
error: Invalid value for "webSearch.maxResults": ...must be >= 1...

(exit code 2)

neo config unset <key>

Removes a key from the on-disk settings.json; the next read falls back to the built-in default (or leaves the key absent if it has none). A no-op (removed: false) when the key was never explicitly set.

neo config validate

Parses settings.json, providers.json, and mcp.json against their schemas and reports ok/errors per file. A missing file is not an error — loadConfig() creates it with defaults — so validate reports absent (defaults apply) rather than failing. Exits 0 when every present file is valid, 1 if any file is invalid.

$ neo config validate
settings.json: ok
providers.json: ok
mcp.json: INVALID
  - Unrecognized key(s) in object: 'foo'

(exit code 1 when any file is INVALID)

Behavior toggles (settings.behavior.*)

CLI-expansion follow-up: the "soul capability" / vision-waves behavior toggles — previously reachable only via environment variables — are now also settable through settings.behavior.*, reachable with the same generic neo config get/set/unset commands (no new command was needed; these are just new dotted-path leaves under settings).

KeyEnv var (unchanged)Default
behavior.dreamingNEO_MEMORY_DREAMINGtrue
behavior.reflectionNEO_REFLECTIONtrue
behavior.attunementNEO_ATTUNEMENTtrue
behavior.heartbeatNEO_HEARTBEATtrue
behavior.signalsNEO_SIGNALStrue
behavior.reflectionFeedbackNEO_REFLECTION_FEEDBACKtrue
behavior.ragDecayRankingNEO_MEMORY_RAG_DECAY_RANKINGtrue

Precedence: env > config > default. When the env var is explicitly set, it always wins — this is back-compat for anyone relying on the env var today (same convention as NEO_MODEL/NEO_PROVIDER's applyProviderEnvOverrides, runtime/config-loader.ts). When the env var is unset, the config value applies; when both are unset, the toggle defaults to enabled. See runtime/behavior-settings.ts for the resolver.

$ neo config set behavior.reflection false
behavior.reflection: (unset) -> false

$ neo config get behavior.reflection
behavior.reflection = false

$ neo config unset behavior.reflection
Removed behavior.reflection from settings.json.

Values are booleans, so use true/false (valid JSON), not on/off (the env vars' own sentinel strings — neo config set parses its value as JSON when possible, and off is not valid JSON, so it would be rejected by the schema as a plain string). A malformed behavior block (e.g. a non-boolean value written by hand-editing settings.json) is stripped on load — same fail-safe policy as microCompact/webSearch/channels — and every toggle falls back to its env/default.

Sessions

Session management — thin adapters over actions/sessions.ts, which wraps the existing session substrate (memory/session-list.ts, memory/session-store.ts). For loading a session INTO a live conversation, use the interactive /resume and /continue slash commands (unchanged) — these are read/export/delete-only management operations.

neo sessions list [--limit N] [--all]

Lists sessions, most-recently-active first. By default scoped to the current working directory (same scoping /continue uses); --all lists every session regardless of cwd (same scope as /sessions list inside the TUI).

$ neo sessions list
ID        TITLE                     MESSAGES  STATUS  LASTACTIVE
8eafb49b  Respond to ping           2         idle    2026-06-01T09:52:40.457Z

$ neo sessions list --all --quiet
8eafb49b-0d93-4170-a14f-7e35c9006d58

neo sessions show <id>

Session metadata plus a preview of the last 5 turns (role + text, truncated to 200 chars each). --json still returns full metadata — only the message previews are truncated, so a long-running session's envelope stays bounded. Use neo sessions export for the untruncated dump. Exits 3 (not found) with a neo sessions list suggestion when the id doesn't exist.

neo sessions export <id> [--out <file>]

Writes the full session (all messages + raw state) as JSON. Defaults to neo-session-<id>.json in the current directory; --out - writes to stdout.

neo sessions delete <id> [--yes]

Deletes a session's stored directory under ~/.neo/sessions/. Without --yes it prints what would be deleted and refuses — no interactive prompt; the flag IS the confirmation (same idiom as neo skills review approve). Refuses outright with a conflict (exit 6) if the session's heartbeat is within the last 60 seconds (the same "alive" window /sessions list uses), to avoid corrupting a live daemon/TUI session — there is no cross-process session lock to consult, so heartbeat recency is the best available liveness signal.

neo sessions archive is not implemented: the session store has no archival notion (no archived flag, no separate archive directory) to build on.

Memory

Memory management — thin adapters over actions/memory.ts, which wraps the auto-memory store (memory/auto-memory/*) and the dreaming pass (memory/distillation/*). Named memory-cli internally (not memory) to avoid colliding with the existing /memory slash command, which opens MEMORY.md in $EDITOR — an independent surface over the same data.

Storage layout:

Project memory (per git repo, all worktrees share one via the canonical git root):
  <neoBase>/projects/<sanitized-git-root>/memory/
    MEMORY.md       — index: "- [Title](file.md) — description" per entry
    <slug>.md       — individual memory file (YAML frontmatter + body)

Global memory (cross-project personal facts):
  <neoBase>/global/memory/
    MEMORY.md
    user_*.md

Scope resolution (--global/--project, default auto): inside a git repo, auto resolves to the project dir; outside, to global — same default as /memory.

neo memory show [--global|--project]

Prints the resolved MEMORY.md index. Reports "No MEMORY.md yet" (exit 0, not an error — an absent index is a normal empty state) rather than failing.

neo memory list [--global|--project] [--auto]

Lists the MEMORY.md index (parsed - [Title](file.md) — description lines). --auto additionally lists the full auto-extracted store — every .md file's parsed frontmatter (id/type/description/tier) — which the index is a hand-curated summary of and can drift from (see the dreaming pass's hook re-index step).

neo memory forget <name-or-id> [--global|--project] [--yes]

Deletes one memory entry: the file itself, plus its MEMORY.md index line (if present). <name-or-id> is the memory file's slug/filename, with or without .md. Without --yes it prints what would be removed and refuses — no interactive prompt; the flag IS the confirmation (same idiom as neo sessions delete). Not found → exit 3 with a neo memory list --auto suggestion. Hard-delete (not archived) — distinct from the dreaming pass's soft-delete archival for stale memories.

neo memory dream [--now]

Runs the memory dream pass (Phase 5.3) on demand: scans active memory and the past week of sessions for archive candidates, resurfacing matches, and contradictory pairs, writing one INSIGHTS.md per scope. Absorbs the legacy neo dream --now, which still works as a deprecated alias — it now prints a one-line notice to stderr (note: \neo dream` is deprecated; use `neo memory dream`.) and delegates to the same action. The notice never touches stdout, so --json`/script consumers of the legacy command are unaffected.

neo memory consolidate (mentioned in the original module map) is not implemented as a standalone command: consolidation only runs as part of the dreaming pass in this codebase — there's no separate entry point to wrap without inventing new semantics.

Cron

Durable scheduled-task management — thin adapters over actions/scheduler.ts, which reuses the exact validation/semantics of the existing CronCreate / CronList / CronDelete LLM-facing tools (tools/scheduler/cron-*.ts) over the shared durable store ~/.neo/scheduler/scheduled_tasks.json — the same file the daemon's own CronScheduler (runtime/daemon/scheduler-host.ts) polls and fires from. Session-only (durable: false) crons live only in the process that created them and have no CLI surface.

neo cron list

All durable cron tasks, most-recently-created first.

$ neo cron list
ID        SCHEDULE              TYPE       PROMPT         ENABLED  NEXTFIRE               LASTFIRE
4d198a58  Every day at 9:00 AM  recurring  daily standup  yes      7/4/2026, 9:00:00 AM   -

neo cron show <id>

Full details for one task (accepts a unique id prefix), including its computed next-fire time and last-fire timestamp (if any).

neo cron create --schedule "<cron>" --prompt "<text>" [--name X]

Provide exactly one of --schedule (5-field cron expression) or --interval ("5m"/"2h"/"1d"). --one-shot fires once then auto-deletes (default: recurring); --permanent exempts a recurring task from the 7-day auto-expiry. --name is echoed back in the confirmation only — CronTask has no name field on disk. Rejects (exit 2) an invalid cron/interval, a schedule with no match in the next year, or when at the 50-task ceiling (same limits as the tool).

neo cron create --schedule "0 9 * * *" --prompt "Post the daily standup summary"
neo cron create --interval 5m --prompt "Check inbox" --one-shot

neo cron delete <id> [--yes]

Same confirmation idiom as neo sessions delete — without --yes, prints what would be deleted and refuses.

neo cron trigger <id>not supported

Always exits 4 (unavailable). Cron fires are internal to whichever process holds the scheduler's ownership lock (normally the daemon's single CronScheduler instance) — its onFire is an in-process closure, not reachable from a separate CLI invocation. The daemon's inbound signal pipe (runtime/daemon/signals.ts) is a closed allowlist (rewake / run-system-tag / run-action) with no "fire this task now" kind, and extending that trust boundary for a single convenience command was judged out of scope. Workaround: delete + recreate with a near-term one-shot schedule, or use neo daemon trigger <job> for the system maintenance jobs (dream/consolidate/reflect/notice), which genuinely do route through the signal pipe.

neo cron history [--last N]

There is no separate fire-ledger file — scheduled_tasks.json only carries a lastFiredAt stamp per task. This reports each task's most recent fire (most-recent-first, default last 20), not a full per-fire log; tasks that have never fired (including every one-shot, which self-deletes on its one fire) are omitted.

Tasks

To-do item management — thin adapters over tasks/task-manager.ts, which already has soft delete + an audit-preserving high-water mark (deleted tasks keep their file and are excluded from listings unless asked for). TaskManager is normally one-per-session; the CLI has no live session to scope to, so neo tasks * operates on one stable, well-known list (~/.neo/tasks/cli/) — independent of any session-scoped list an agent run creates. This mirrors how neo cron * operates on the one durable, global scheduler file rather than a session-scoped one.

neo tasks list [--status <s>]

--status is one of pending, in_progress, completed, deleted (deleted implicitly includes soft-deleted rows, otherwise hidden).

neo tasks show <id>

Full task details (subject, description, status, owner, blocks/blockedBy, timestamps). Exits 3 (not found) with a neo tasks list suggestion.

neo tasks create <title> [--description]

--description defaults to the title when omitted (the storage layer requires a non-empty description).

neo tasks update <id> [--status|--title]

At least one of --status (pending/in_progress/completed — not deleted, use neo tasks delete for that) or --title is required. Refuses (conflict, exit 6) to update an already soft-deleted task.

neo tasks delete <id> [--yes]

Soft-delete: same confirmation idiom as neo sessions delete/neo cron delete. The task's file is kept (status becomes deleted) for audit — neo tasks show <id> still works afterward. Deleting an already-deleted task is a conflict (exit 6), not a silent no-op.

Daemon

neo daemon <run|stop|install|uninstall> (legacy routing — see neo daemon --help) plus the framework-registered subcommands below (neo daemon <cmd> --help for full flag reference).

neo daemon status

Health (HEALTHY/STALE/DEAD/FOREIGN), owner (pid/version/host), heartbeat age, capability tier, today's budget, supervised channels, and log/audit paths. Same data as the top-level neo status daemon section, framed as its own command. Non-zero exit (4, UNAVAILABLE) when the daemon isn't healthy.

$ neo daemon status --quiet
dead

neo daemon logs [--tail N] [--follow]

Read (or tail/follow) daemon.log.

neo daemon logs --tail 50
neo daemon logs --follow      # polls for appended lines every 500ms; Ctrl-C exits

--follow is human-output only; combine with --json/--quiet for a single point-in-time snapshot instead.

neo daemon audit [--last N]

Show recent entries from the forensic audit log (audit.jsonl — autonomous job started/completed/failed). Default: last 20. Human mode renders a table (time, job, phase, summary); --json returns the full event objects.

neo daemon audit --last 5

neo daemon budget

Today's token ledger: spent, outstanding reservations, remaining headroom under the configured NEO_DAEMON_DAILY_TOKEN_BUDGET cap (null limit/remaining when uncapped).

$ neo daemon budget
Day:       2026-07-03
Spent:     18500 tokens
Reserved:  0 tokens
Limit:     500000 tokens
Remaining: 481500 tokens

neo daemon trigger <job>

Ask a RUNNING daemon to run a system maintenance pass now (dream, consolidate, reflect, or notice — the same tags the daemon's own scheduler uses; consolidate/reflect/notice are currently reserved no-ops downstream, same as the scheduler). Submits a run-system-tag signal through the existing inbound-signal IPC (runtime/daemon/signals.ts) — the daemon's signal-consumer drains it within one poll tick (~1s). Requires a live daemon; exits 4 (UNAVAILABLE) with a neo daemon run hint otherwise.

neo daemon trigger dream

UI dashboard

See ui-customization.md for the full ui.json declarative-widget reference (schema, worked examples, safety model).

neo ui [--port N] [--no-open] [--token-only]

Open the control-plane dashboard. If a daemon is running, its control plane is already listening — this prints the dashboard URL (and opens a browser) without binding a second server. With no daemon, starts a standalone in-process control plane (same routes/engine options the daemon uses) and keeps running until Ctrl+C. --token-only prints just the bearer token (scripts); --no-open skips the auto browser-open (also skipped automatically on a non-TTY stdout). Not exposed over the command-RPC API — a UI command asking the API to open a UI is circular, and this command is process-owning/browser-launching, unlike everything else in this group.

$ neo ui --no-open
No daemon running — started a standalone control plane.
Dashboard: http://127.0.0.1:4747/#token=...
Press Ctrl+C to stop.

neo ui validate [--path <file>]

Parse ui.json against the declarative UI-config schema and report every violation with its JSON path. A missing file reports valid:true (nothing to validate). This is a report, not an action — the envelope is always {ok:true, data:{valid,errors}} — but the CLI's own exit code goes non-zero on valid:false, so it's script-gateable.

$ neo ui validate
~/.neo/config/ui.json: invalid (1 error):
  pages[0].widgets[1].endpoint: Required

neo ui show [--path <file>]

Print the resolved config (or the built-in default) and its source (file/default/last-good) — the same resolution GET /api/ui-config performs, computed directly from disk with no daemon needed.

$ neo ui show --quiet
file

Workflows

Thin adapters over actions/workflows.ts, which wraps the existing engine (workflows/storage.ts, workflows/approval-queue.ts, workflows/executor.ts).

neo workflows list

Lists on-disk workflow definitions (~/.neo/workflows/<id>/definition.yaml): id, name, version, first trigger kind (+count), step count. A single corrupt definition is skipped, not fatal to the list.

neo workflows runs [--last N]

Lists workflow runs, newest-first (sorted by createdAt). --last N caps to the most recent N.

neo workflows approvals

Lists pending approvals awaiting a decision. Same data as the legacy neo workflows list-pending-approvals — this is the preferred spelling going forward (see "Legacy routing" below for why both keep working).

neo workflows approve <id> / neo workflows reject <id>

Resolve a pending approval and resume the run's step loop. reject is not a separate queue-layer API — it resolves the SAME approval record as approve, with decision.choice = 'reject' (the ApprovalChoice union already includes 'reject' as a valid decision passed to the one ApprovalQueue.resolve() path). Uses an empty CapabilityRegistry to re-enter the step loop (a one-shot CLI process has no in-memory tool bindings) — safe when the approval is the run's last blocking step; if remaining steps need capability/sub-agent execution, the executor throws a clear error rather than silently no-op'ing.

neo workflows approve 3f9c2b7a-...
neo workflows reject 3f9c2b7a-...

neo workflows trigger <name>

Ask a RUNNING daemon to dispatch a workflow now, via a run-action signal (tier 2) through the same inbound-signal IPC neo daemon trigger uses for system tags (runtime/daemon/signals.ts). There is no direct/non-daemon dispatch path — a capability- or sub-agent-bearing workflow needs the daemon's tier-scoped CapabilityRegistry and restricted AgentDependencies (runtime/daemon/workflow-host.ts) to execute safely unattended, and only the daemon's composition root builds those. Requires a live daemon; exits 4 (UNAVAILABLE) with a neo daemon run hint otherwise. Validates the workflow id against on-disk definitions first, so a typo fails fast (3, NOT_FOUND) instead of queuing a signal nothing will act on.

neo workflows trigger daily-digest

Teams

Read-only over teams/team-store.ts + teams/mailbox.ts. No mutation commands here — team creation/membership is a Tool-layer concern.

neo teams list

Lists every team on disk: sanitized name, display name, member count, created.

neo teams show <name>

Full team config (lead, description, roster) plus a per-member mailbox summary (unread/total message counts — a cheap inbox scan, no message bodies fetched). <name> may be the display name or the sanitized on-disk name.

Skills

Review skill candidates the dreaming pass drafts into ~/.neo/skills-candidates/<name>/ (a directory the skill loader never scans by design). Both spellings below call the SAME actions/skills.ts functions.

neo skills list / neo skills show <name>

List waiting candidates (name, description, drafted-at), or print one candidate's full SKILL.md (frontmatter + body) for review before approving.

neo skills approve <name> [--yes] / neo skills reject <name>

approve moves the candidate into ~/.neo/skills/<name>/ so it becomes a live skill, and clears this process's skill-loader cache. Requires --yes (non-interactive confirmation — there is no TTY prompt); without it, exits 2 (USAGE) after validating the candidate exists. reject permanently deletes the candidate. Approval does not hot-reload an already-running daemon/session — a live daemon picks up the new skill on its own next populateSkillRegistry() call (today: daemon boot).

neo skills show research-helper
neo skills approve research-helper --yes

Live-skill management: neo skills active / disable / enable / remove

These manage live skills — SKILL.md files actually loaded into the agent via loadSkills() — as opposed to neo skills list above, which only shows review candidates. neo skills list's meaning is unchanged; these are new, separate verbs.

  • neo skills active — lists every skill loadSkills() merges from the user/project/plugin tiers (name, source, description), annotated with disabled state. Disabled skills are shown, not hidden, so this is the full on-disk picture. Bundled (built-in) skills are not shown — they have no on-disk directory to toggle or delete.
  • neo skills disable <name> — the primary, reversible control. Adds <name> to settings.skills.disabled; loadSkills() then filters it out entirely, so its description is never sent to the model — this is a token-economy lever, not a safety one. Clears this process's skill-loader cache. Exits 3 (NOT_FOUND) for an unknown name.
  • neo skills enable <name> — removes it from settings.skills.disabled, restoring it on the next load.
  • neo skills remove <name> [--yes]destructive: deletes ~/.neo/skills/<name>/ outright. Only works on user-scope skills; project-local, plugin-contributed, and built-in skills are refused with a clear error (use disable for those — it works uniformly across every source). Requires --yes, same non-interactive-confirmation model as skills approve.

None of these hot-reload an already-running daemon/session in a different process — same cross-process caveat as skills approve.

neo skills active --json
neo skills disable research-helper
neo skills enable research-helper
neo skills remove old-skill --yes

Channels

Daemon-side supervision ops over the channel connectors (Telegram, WhatsApp) neo daemon run spawns per settings.channels.supervise. Credential management (login/list/logout) stays on the legacy neo channels surface — see Auth & providers below.

neo channels status

Per-channel connector state/pid/restarts/uptime, read from the same channel-status.json snapshot neo daemon status's channel rollup already parses (runtime/daemon/channel-status.ts). Works whether or not the daemon is running — no supervised channels is a valid, empty report.

$ neo channels status
Daemon: healthy
CHANNEL   STATE  PID    RESTARTS  UPTIME  NOTE
telegram  up     13092  0         12356s

neo channels logs <channel> [--tail N]

There is no per-channel log file on disk — every connector's stdout/stderr is piped into the shared daemon.log, prefixed [channel:out]/[channel:err] (runtime/daemon/channel-supervisor.ts). This is a filtered view of that shared file (lines starting with [<channel>:), not an independent log.

neo channels restart <channel>not supported

Connector restart is internal to the daemon's channel-supervisor (exponential backoff + crash-loop cap fired from the child's own exit handler) — there is no external entry point for it, and the daemon's signal pipe (rewake/run-system-tag/run-action) has no "restart channel X" kind. Adding one would mean extending that closed allowlist's trust boundary for a single convenience command — the same tradeoff neo cron trigger already declined (see Cron section above). Always exits 4 (UNAVAILABLE) with the real workaround: restart the whole daemon (neo daemon stop then neo daemon run, or neo daemon install to reinstall the autostart service) — it respawns every supervised, credentialed channel from scratch.

Plugins

Thin adapters over actions/plugins.ts, which shares the discovery/install/ trust logic the /plugins slash command (cli/commands/plugins.ts) uses — but does NOT share its live state. /plugins runs inside a live EmbeddedNeo session with a hot PluginRegistry and can dispatch enable/disable in-process so contributions appear without a restart. A one-shot neo plugins * CLI invocation has no such process: list/show scan ~/.neo/plugins/ directly (same loadPluginFromDirectory helper runPluginBootstrap uses at startup), and enable/disable are settings-only — a running daemon or session picks up the change on its next start, same as neo skills approve's documented behavior. Named plugins-cli internally (not plugins) to avoid colliding with the existing slash-command module — same split pattern as sessions-cli/ memory-cli/tasks-cli.

neo plugins list

Every plugin installed under ~/.neo/plugins/, whether or not it's currently in settings.json's enabledPlugins. A corrupt manifest is skipped, not fatal to the list (matches runPluginBootstrap's per-plugin error isolation).

$ neo plugins list
NAME         VERSION  ENABLED  SOURCE  CONTRIBUTIONS
demo-plugin  1.0.0    no       user    no contributions

neo plugins show <name>

Full detail for one installed plugin: version, description, source, path, enabled state, skill dirs, MCP server names, output-styles/commands dirs. Not installed → exit 3 with a neo plugins list suggestion.

neo plugins enable <name> [--trust] / neo plugins disable <name>

enable reuses enablePluginWithApproval verbatim: a plugin already trusted at its current manifest digest enables without a prompt; otherwise this asks y/n interactively (TTY only), or --trust records trust and skips the prompt (CI / autonomous use). disable removes the name from enabledPlugins. Neither attempts in-process dispatch/teardown — there is no live plugin registry to dispatch into from a one-shot CLI process.

neo plugins enable demo-plugin --trust
neo plugins disable demo-plugin

neo plugins install <name> [--force] [--trust] / neo plugins install --from <owner>/<repo>[/<subdir>] [--trust]

Installs a bundled template (templates/plugins/<name>/) or clones from GitHub (git required on PATH), then runs the same approval gate as enable. --force overwrites an existing bundled install; GitHub installs always overwrite (an explicit --from spec is opting in to whatever is at that source). Reuses runInstall/runInstallFromGitHub verbatim.

neo plugins install fetch-helper
neo plugins install --from anthropics/claude-plugins-official/code-review --trust

neo plugins uninstall <name>

Removes the plugin directory (preserving any manifest.persistentData paths), clears enabledPlugins + the trust store, and warns (non-blocking) if another installed plugin declares a reverse dependency on it. Reuses runUninstall verbatim, with this module's own disk-scanned reload of every other installed plugin standing in for the live registry /plugins uninstall would otherwise consult.

neo plugins enable --accept-existing (bulk-trust grandfathering) is not implemented at this layer — a one-time migration path, not a steady-state CLI verb. Enable plugins one at a time instead.

MCP

Thin adapters over actions/mcp.ts, reading/writing the same <NEO_HOME>/config/mcp.json the pre-existing /mcp slash command (cli/commands/mcp-add.ts/mcp-remove.ts/mcp-config.ts) already uses, validated with the real McpConfigSchema. Named mcp-cli internally (not mcp) to avoid colliding with the existing slash-command module.

Configuration only, not live status: /mcp list//mcp status inside a session report a connected McpServerRegistry — actual tool/resource/prompt counts from a running client. neo mcp * never spins up a real MCP client connection (stdio subprocess or HTTP handshake) just to answer a listing — that would be a real side effect for a status check. list/show report the configured shape (transport, command/url, env/header keys with secrets redacted) instead. For live counts, use /mcp status in the TUI or neo daemon status.

neo mcp list

All servers configured in mcp.json: name, transport, command/url, whether OAuth is configured.

$ neo mcp list
NAME   TRANSPORT  TARGET                    OAUTH
fetch  stdio      npx                       no

neo mcp show <name>

Full (redacted) config for one server — command/args/cwd for stdio, url for sse/http/ws, env/header values matching /key|token|secret|password |authorization/i rendered as <redacted>. Not found → exit 3.

neo mcp add [preset]

Without a preset name, lists the bundled starter-preset catalogue (mcp/starter-presets.ts) with descriptions and whether each needs OAuth. With a name, appends that preset's config to mcp.json — rejects an unknown preset (exit 2) or a name collision with an already-configured server (exit 6, conflict). Does not connect; restart Neo (or use the interactive /mcp connect <name>) to activate. If the preset requires OAuth, follow up with the interactive /mcp auth <name> (see below).

neo mcp add                # list presets
neo mcp add fetch          # add the "fetch" preset

neo mcp remove <name>

Removes a server from mcp.json. Only edits the config file — there is no running connection to stop from a one-shot CLI process. Not found → exit 3.

neo mcp auth <name>not re-registered on the framework

The OAuth flow (cli/commands/mcp-auth.ts) opens a browser and blocks on a loopback callback — inherently interactive, which conflicts with the action layer's "never TTY" design rule (plans/cli-expansion/_master-plan.md §1.1). Use the interactive /mcp auth <name> slash command inside a session instead; this mirrors how provider login also stays TTY-side per the master plan's action-layer module map.

Permissions

Inspect and edit the permission rules that gate tool calls (permissions/loader.ts + permissions/engine.ts — the same merge and decision engine the agent loop uses at boot). allow/deny/remove write ONLY the user-scope file (~/.neo/config/permissions.json); the project (./.neo/permissions.json) and local (./.neo/settings.local.json) scopes are shown by list but stay edit-by-hand — a CLI-side mutation of a shared project file would silently affect every other user of that project, a different trust boundary than a per-user default.

neo permissions list

Merged mode + rules across all three scopes, with each scope's source file path. --json/--quiet supported.

neo permissions allow <pattern> / neo permissions deny <pattern>

Append a rule string (e.g. Bash(git:*), Read(/etc/**), or a bare tool name) to the user-scope file under that behavior. Idempotent — re-adding an existing pattern reports "already present" instead of duplicating it.

neo permissions remove <allow|deny|ask> <pattern>

Remove a rule from the user-scope file. Matches the pattern as stored OR its canonical Tool(content) form, so escaping differences don't cause a silent miss. Reports removed: false (exit 0) rather than erroring when the pattern isn't found.

neo permissions explain <tool-name> [--input '<json>']

Runs the real decision engine against a synthetic tool call under the CURRENT effective policy — "what would happen right now" — without invoking the tool. Reports the mode, the decision (allow/deny/ask), and which rule (if any) matched.

neo permissions allow "Bash(git:*)"
neo permissions deny "Bash(rm -rf *)"
neo permissions explain bash --input '{"command":"rm -rf *"}'
# Decision: DENY  Reason: rule (userSettings: bash(rm -rf *))
neo permissions remove deny "Bash(rm -rf *)"

Descoped: neo permissions denials (per-decision denial audit log) — there is no durable denial ledger in the codebase (permissions/denial-tracking.ts is in-memory, per-session state for the auto-mode resolver's fallback-to-prompt threshold, not a persisted log). neo daemon audit is the closest existing analogue for the always-on posture.

Auth

neo auth status

Per-provider auth status (logged in? which method? which store?) — never prints secret material. Shares actions/auth.ts:getAuthStatus with the legacy neo login --status, so the two report identical per-provider strings; this command just renders them under the framework's table/--json/--quiet conventions.

$ neo auth status
PROVIDER  NAME              STATUS
claude    Anthropic Claude  API key (or unauthenticated)
openai    OpenAI            API key (or unauthenticated)
gemini    Google Gemini     (not configured)

Internal: ProviderMethodResolver (actions/auth.ts) abstracts "which provider, which auth method" resolution behind an interface — DefaultProviderMethodResolver resolves strictly from explicit arguments, throwing an ActionError (never prompting) when a choice is ambiguous. This is the seam a future non-TTY caller (HTTP handler, SDK embed) can use as-is; neo login's interactive readline picker (cli/commands/login.ts) is intentionally NOT migrated onto the framework — see "Auth & providers (legacy routing)" below — but satisfies the same shape, just prompting instead of throwing when a choice is missing.

Providers

Manage per-provider credentials in ~/.neo/config/providers.json (no zod schema exists for this file — reads/writes are structural, matching how config-loader.ts's own loadProviders() reads it). Migrated onto the framework this chip (Chip 11) from the legacy cli/commands/providers.ts module, which is now a thin wrapper over the same actions/providers.ts functions — list/set/remove are byte-for-byte the same behavior as before, now with --json/--quiet/--help support. Never prints a stored API key's value — only presence/absence and storage location (keychain / env:<VAR> / oauth / cli-reuse / legacy plaintext with a warning), same redaction discipline as neo config/neo mcp.

neo providers list

Every configured provider: storage location, status, and which one is the default (marked (default)).

neo providers show <provider-id>

One provider's full config detail (baseUrl, models list, storage/status) — new in Chip 11, no legacy equivalent existed.

neo providers set <provider-id>

Store an API key, read from stdin only (echo KEY | neo providers set claude, or an unechoed TTY prompt). Stored in the OS keychain when available, else ~/.neo/.env.

neo providers remove <provider-id>

Delete a provider's stored API key.

$ neo providers list
ID                STORAGE                STATUS     MODEL
claude (default)  env:ANTHROPIC_API_KEY  (not set)
$ echo sk-ant-... | neo providers set claude
✓ API key for 'claude' saved to OS keychain

Legacy spellings still routed to the (now delegating) legacy module: the rm alias, bare neo providers (defaults to list), and the login/logout redirect-error messages pointing at the top-level neo login/neo logout commands — see "Auth & providers (legacy routing)" below.

Model

neo model show

The resolved default provider + model straight from providers.json. Flags whether NEO_MODEL/NEO_PROVIDER is set in the CURRENT shell — those env overrides apply in-memory only at agent boot (config-loader.ts's applyProviderEnvOverrides) and are never persisted, so the file value may not match what the next neo run actually uses.

neo model set <model-id>

Persists a new top-level defaultModel to providers.json — the one new write seam Chip 11 added (composed from the already-exported readProvidersJson/writeProvidersJson, not a new mutator). Does not validate the id against any provider's models list (informational, not an enum) and does not affect an already-running neo process — picked up on next boot.

$ neo model show
provider: claude
model:    claude-sonnet-4-20250514
config:   ~/.neo/config/providers.json
$ neo model set claude-opus-4-20250514
defaultModel: claude-sonnet-4-20250514 -> claude-opus-4-20250514

Stats

neo stats [--errors] [--tools]

Usage/cost summary read from ~/.neo/telemetry/events.jsonl — the exact same file and observability/telemetry-stats.ts helpers the separate neo-stats binary already reads (that binary stays installed; neo stats is the new, additive top-level spelling per the master plan's backward-compatibility note). Reports: total events, per-provider call counts, cost-by-MODEL (calls/tokens/estimated USD — there is no durable per-provider cost aggregate anywhere in the codebase, only per-model, so "by provider" is call-count only), and today's daemon token budget (actions/daemon.ts:getBudget()). --errors/--tools add the error-code breakdown and tool-call frequency sections. A missing telemetry file (fresh install, or telemetry disabled) is a valid empty summary, not an error.

$ neo stats
Total events: 42

By provider (call count):
  PROVIDER  CALLS
  claude    30
  openai    12

Cost by model:
  MODEL                   CALLS  INTOK  OUTTOK  ESTUSD
  claude-opus-4-20250514  30     15000  6000    $0.675000

Today's budget: 12000 spent / 2000000 limit, 1988000 remaining

Tools (live in-session context control)

Curate which tools are active so irrelevant ones stop consuming their full JSON schema in every turn's tools[] array (real token economy). See plans/live-context-control-design.md for the full design.

neo tools list

Lists statically-enumerable built-in tools with each one's source, ~tokens/turn estimate (a chars/4 approximation of the serialized name + description + inputSchema, the same convention the providers use — hence the ~), global disabled state, and whether it can ever be disabled. MCP tools are not enumerated offline (their schemas only exist after a live server handshake — same descope as neo mcp list); use /tools inside the TUI to see the live set, or neo config set disabledTools '["<mcp-tool-name>"]' to disable one by name.

neo tools disable <name> [--dry-run] / neo tools enable <name>

Globally disable/enable a tool by writing settings.disabledTools. disable always prints a cost preview first — frees ~N tok/turn, one-time ~M tok cache re-write (the tools[] suffix that re-writes when a tool is removed), net-positive after K turns — then applies; --dry-run prints the preview only. Always-load / communication / ToolSearch tools are refused (the agent must not brick its own loop). A running daemon/session picks up the change on its own next boot, not instantly.

$ neo tools disable web-search
Cost preview for "web-search":
  frees ~410 tok/turn
  one-time ~1180 tok cache re-write
  net-positive after 3 turn(s)
Disabled "web-search" — it will no longer be sent to the model.

Interactive surfaces — /tools and /skills (TUI)

Inside the REPL, /tools and /skills open multi-select pickers to toggle tools/skills for the current session (default) or globally, with the cost preview shown before you commit (Space toggles, Enter commits, Esc cancels). Per-session overrides win over the global default in both directions — force-ON a globally-disabled item just for this session, or force-OFF a globally-enabled one. Inline forms: /tools disable <name> [--global|--session], /skills disable <name> [--global|--session], and enable likewise. Per-session choices persist across /resume and /continue. Toggles take effect on the next turn (never mid-turn — a prompt-cache-safety invariant). After a tool goes unused for unusedToolSuggestThreshold turns (default 15), Neo posts a single non-blocking suggestion to disable it — you always decide; nothing is auto-disabled. Skills are pay-per-use, so disabling one saves only its single catalog line, not a per-turn schema — the picker says so honestly.

Auth & providers (legacy routing)

  • neo login [provider] [--oauth|--api-key|--status] — interactive picker (readline TTY prompt); stays legacy by design (discovery C-arch-readiness.md §4.1 — cannot be called from a non-TTY interface). --status shares actions/auth.ts:getAuthStatus with neo auth status above (byte-identical per-provider output).
  • neo logout <provider>
  • neo providers <rm|login|logout> (+ bare neo providers, defaulting to list) — list/set/remove/show are framework-routed (see "Providers" above); only the rm alias and the login/logout redirect-error messages have no framework equivalent.
  • neo channels <login|list|logout> — credential management (bot token, owner id) for each channel; unrelated to the neo channels status|logs| restart supervision ops above. neo channels (bare, no subcommand) defaults to list. The old channels status <channel> credential-presence probe was retired in Chip 10 in favor of neo channels status's daemon supervision rollup — neo channels list already reports the same credential presence/absence per channel.

Agent & sessions (legacy routing)

  • neo attach [message] — connect to the running daemon engine.
  • neo dream [--now] — manual memory consolidation pass.
  • neo profile <show|regenerate>
  • neo contacts <add|list|remove>
  • neo live-queue — telemetry tuning queue.

Workflows & skills (legacy routing — spellings kept working, see below)

  • neo skills review [list|show|approve|reject] — the original review-prefixed spelling (vision-waves W4.3). neo skills list|show|approve|reject (no review token) is the shorter framework-routed spelling added in Chip 7 — both call the same actions/skills.ts functions. bin/neo.ts pre-checks args[1] === 'review' BEFORE framework dispatch, since the framework registry considers skills a group (skills list etc. are registered) and would otherwise render group-help + exit(2) for an unrecognised review token instead of falling through.
  • neo workflows status <runId> / neo workflows list-pending-approvalsstatus (the run-detail view) has no framework equivalent by design; list-pending-approvals is the pre-Chip-7 spelling of neo workflows approvals, kept working verbatim rather than aliased. Same pre-framework-dispatch narrowing in bin/neo.ts as the skills review case above, for the same reason (workflows list/runs/... registration makes workflows a group too).
  • neo workflows runs / neo workflows approve <id> also have legacy implementations in cli/commands/workflows.ts, but those spellings are now claimed by the framework registry (workflows-cli.ts) — the legacy code path is unreachable from the CLI for those two, kept only for direct module callers (e.g. its own unit tests).

For interface developers

Framework commands are thin adapters over src/actions/* — plain async functions returning typed structs and throwing ActionErrors. A future UI / HTTP API / MCP server calls the same functions; the --json envelope above is the wire contract. Agent runs use the existing EmbeddedNeo.sendMessage + AgentEvent stream instead. See plans/cli-expansion/_master-plan.md §1.