# Installing EONYX: build, run, and configure


**EONYX** is a terminal AI agent for working with code, written in [Go](https://go.dev).
It connects straight to any OpenAI-compatible API.
[OpenAI](https://openai.com), [OpenRouter](https://openrouter.ai), [DeepSeek](https://www.deepseek.com), [Moonshot](https://www.moonshot.ai).
Or a local [Ollama](https://ollama.com), [llama.cpp](https://github.com/ggml-org/llama.cpp) or [LM Studio](https://lmstudio.ai) server.
No telemetry, no OAuth.
One process, one binary.

<!--more-->

## 📦 Requirements and installation

### What you need

- **[Go 1.26+](https://go.dev/dl/)** to build from source. A prebuilt binary needs nothing.
- **An API key** for any OpenAI-compatible provider. A local model server needs no key at all.
- A terminal: [Windows Terminal](https://aka.ms/terminal), [PowerShell](https://learn.microsoft.com/en-us/powershell/), any Linux shell, macOS Terminal.
- **A licence.** One key binds one machine, and it is verified at every startup.

### Installation options

{{< tabs defaultTab="0" >}}
{{< tab title="From source" markdown="true" >}}

```bash
git clone https://github.com/eonyx-core/eonyx
cd eonyx
make build        # builds bin/eonyx with the version from the git tag
```

Or install into `$GOPATH/bin`:

```bash
make install
```

The version is baked in at build time from `git describe --tags`.
Nothing is hardcoded in the code:

```bash
eonyx --version
```

{{< /tab >}}
{{< tab title="Prebuilt binary" markdown="true" >}}

[GoReleaser](https://goreleaser.com) builds a release on every `v*` tag.
Grab the archive for your platform from the [Releases](https://github.com/eonyx-core/eonyx/releases/latest) page and put the binary on your `PATH`.
Linux, macOS and Windows are covered.

{{< /tab >}}
{{< /tabs >}}

### First run

```bash
cd my-project
eonyx
```

The interactive TUI opens.
With no config at any level, EONYX writes a working `eonyx.json` into the global directory itself.
It ships two DeepSeek models and one local Ollama one.
All that is left is the key:

```bash
export DEEPSEEK_API_KEY=sk-...   # or your own provider, see below
```

{{< admonition type="tip" title="Headless mode for scripts and CI" >}}
`run` does the task without the TUI and exits:

```bash
eonyx run "describe what this repository does"
eonyx run -m deepseek-v4-pro --yolo "fix the failing test"
```

It works in a pipe too.
The argument beats stdin, so when piping do not repeat the prompt as an argument.
Otherwise stdin is ignored:

```bash
{ echo "explain the error:"; cat error.log; } | eonyx run
```

The answer streams to **stdout**.
Tool activity and token stats go to **stderr**.
So redirecting into a file or a pipe is safe.

Flags:

- `--model/-m` — the model
- `--session/-s <id>` — continue a specific session
- `--continue/-c` — continue the most recent one
- `--yolo` — disable tool confirmations
- `--json` — events as NDJSON instead of text
- `--system-prompt` — replace the system prompt

Exit codes tell outcomes apart for CI:

- `0` — success
- `2` — usage error
- `3` — API error
- `4` — the task finished, but a tool was blocked by the permission gate
- `5` — hit the `max_steps` limit
- `130` — interrupted with Ctrl+C
{{< /admonition >}}

### The remaining subcommands

| Command          | Purpose                                    |
| ---------------- | ------------------------------------------ |
| `eonyx`          | interactive TUI chat                       |
| `eonyx run "…"`  | headless agent run                         |
| `eonyx sessions` | list of saved sessions                     |
| `eonyx info`     | configuration, data, and project paths     |
| `eonyx config`   | the final (merged) configuration           |
| `eonyx clean`    | remove its own configuration files         |

### Keys and commands in the TUI

| Key       | Action                                       |
| --------- | -------------------------------------------- |
| `Enter`   | send the message                             |
| `Ctrl+J`  | line break in the input field                |
| `↑ / ↓`   | history of your messages                     |
| `Esc`     | interrupt the running turn                   |
| `/`       | command palette (on empty input)             |
| `@`       | file path autocompletion (Tab inserts)       |
| `?`       | help (on empty input)                        |
| `Ctrl+P`  | command palette                              |
| `Ctrl+N`  | switch model                                 |
| `Ctrl+S`  | session manager                              |
| `Ctrl+K`  | skills and MCP servers (on/off)              |
| `Ctrl+O`  | expand/collapse tool output                  |
| `Ctrl+R`  | expand/collapse the model's reasoning        |
| `! <cmd>` | run a shell command                          |
| `Ctrl+C`  | quit                                         |

Slash commands: `/new`, `/clear`, `/add [path]`, `/rename <title>`, `/fork`, `/export`, `/yolo`, `/plan`, `/timestamps`, `/model`, `/sessions [query]`, `/help`, `/quit`.

{{< admonition type="note" title="The '!' command" >}}
Output from a `!` command is written into the conversation.
The model then sees what you ran and what changed, such as the directory or an environment variable.
{{< /admonition >}}

## 🧭 Configuration

EONYX reads `eonyx.json` from **three levels** and merges them top down.
Each level overrides the one before it.

- **Global config**
  - Windows — `%LOCALAPPDATA%\eonyx\eonyx.json`
  - Linux/macOS — `$XDG_CONFIG_HOME/eonyx/eonyx.json` (`~/.config/…` by default)
- **Global data**
  - Windows — the same path
  - Linux/macOS — `$XDG_DATA_HOME/eonyx/eonyx.json` (`~/.local/share/…` by default)
- **Project config**
  - `eonyx.json` or `.eonyx.json`, found by walking up from the current directory

Wherever the project config is found becomes the **project root**.
A service folder `.eonyx` for exports and logs appears inside it.
`EONYX_GLOBAL_CONFIG`, `EONYX_GLOBAL_DATA` and `EONYX_CACHE_DIR` override the paths.
`eonyx info` and `eonyx config` show what was loaded and from where.

{{< admonition type="warning" title="Strict schema" >}}
The parser rejects unknown fields, wrong types and malformed JSON.
EONYX **refuses to start** rather than quietly ignore a typo.
An unknown field is reported with its name and line number.
A wrong type or broken JSON gets a generic message.
{{< /admonition >}}

### A full eonyx.json example

This is the default config written on the first run.
It carries every supported top-level key:

```json
{
  "providers": {
    "deepseek": {
      "type": "openai",
      "base_url": "https://api.deepseek.com",
      "api_key": "$DEEPSEEK_API_KEY"
    },
    "local": {
      "type": "openai",
      "base_url": "http://localhost:11434/v1",
      "api_key": ""
    }
  },
  "models": [
    {"id": "deepseek-v4-flash", "provider": "deepseek", "context_window": 1000000, "input_price": 0.14, "output_price": 0.28},
    {"id": "deepseek-v4-pro", "provider": "deepseek", "context_window": 1000000, "input_price": 0.435, "output_price": 0.87},
    {"id": "qwen2.5-coder", "provider": "local", "context_window": 32768, "input_price": 0, "output_price": 0}
  ],
  "default_model": "deepseek-v4-flash",
  "max_steps": 50,
  "permissions": {
    "allowed_tools": []
  },
  "skills": [],
  "mcp": [],
  "hooks": {},
  "report": {
    "database_url": ""
  },
  "system_prompt": "",
  "log_file": "",
  "retry": {}
}
```

### Providers and models

A provider is a way to reach an API.
There is one type so far, `"openai"`, and it covers any OpenAI-compatible endpoint:

```json
"providers": {
  "openrouter": {
    "type": "openai",
    "base_url": "https://openrouter.ai/api/v1",
    "api_key": "$OPENROUTER_API_KEY"
  }
}
```

`api_key` and `base_url` may reference environment variables such as `$OPENAI_API_KEY`.
They expand after the layers merge, so no secret sits in the file.

A model binds an id to a provider:

| Field            | Purpose                                                              |
| ---------------- | -------------------------------------------------------------------- |
| `id`             | the model name as the API understands it                             |
| `provider`       | a key from the `providers` block                                     |
| `context_window` | context size in tokens — **triggers auto-summarization** of history  |
| `input_price`    | price per 1M input tokens (USD), for the cost counter in the header  |
| `output_price`   | price per 1M output tokens (USD)                                     |

`max_steps` caps the tool-call rounds inside one turn.
The default is 50.

### Permissions

The safe tools are always allowed: `read`, `ls`, `glob`, `grep`, `git_status`, `git_diff`, `git_log`, `todo_write`.
Everything else needs permission: writing, editing, bash, commits, MCP tools.

```json
"permissions": {
  "allowed_tools": ["bash", "write", "edit"],
  "sandbox": true
}
```

- **`allowed_tools`** — tools that run without asking.
- **`sandbox`** — locks `read`, `write`, `edit`, `multi_edit` and `ls` inside the project root. Off by default.
- **`--yolo`**, or `/yolo` in the TUI — drops the gate for the whole session.

The TUI adds an interactive layer on top.
A tool outside the allowlist raises an **Allow once / Allow always (this session) / Deny** dialog.
A denial does not kill the turn: the model gets a tool error and adapts.

{{< admonition type="tip" title="Planning mode" >}}
`/plan` switches on a stricter gate.
Only read tools stay available, so the agent studies the code and proposes a plan.
When the plan is ready EONYX asks: **implement it**, **allow edits**, or **keep planning**.
{{< /admonition >}}

### Hooks

Hooks are shell commands around tool calls.
The format is tool name → `before` / `after`:

```json
"hooks": {
  "write": {
    "before": "echo \"writing: $EONYX_INPUT\" >> .eonyx/audit.log",
    "after":  "gofmt -l ."
  },
  "bash": {
    "before": "[ \"$EONYX_CWD\" = \"$HOME\" ] && exit 1 || exit 0"
  }
}
```

Rules:

- **`before`** runs first. A non-zero exit code **blocks** the call.
- **`after`** runs on success. Its output is appended to the tool result, so the model sees it. An error there does not break the result.
- Context arrives **through environment variables**, never substituted into the command text. Injection through the input is impossible.
  - `EONYX_TOOL` — the tool name
  - `EONYX_PHASE` — `before` / `after`
  - `EONYX_CWD` — the working directory
  - `EONYX_INPUT` — the JSON input
  - `EONYX_RESULT` — the result, only in `after`
- Each hook gets **30 seconds**, in the same shell session the `bash` tool uses.

### Other keys

| Key             | Purpose                                                                     |
| --------------- | --------------------------------------------------------------------------- |
| `skills`        | additional skill directories (see the Skills article)                       |
| `mcp`           | the list of MCP servers (see the MCP article)                               |
| `report.database_url` | [PostgreSQL](https://www.postgresql.org) for the read-only reporting tools<br>empty — the tools are not registered |
| `system_prompt` | a full replacement for the generated system prompt                          |
| `log_file`      | a log path instead of the standard one; supports `$ENV`                     |
| `retry`         | `max_retries` — 3 by default<br>`base_delay_ms` — 500, doubles up to 30 s<br>`response_header_timeout_sec` — 60 |

## 📋 Summary

{{< admonition type="abstract" title="Cheat sheet" >}}
- **Installation**
  - `make build` from source or a binary from GitHub Releases
  - check: `eonyx --version`
- **Running**
  - `eonyx` — interactive TUI
  - `eonyx run "task"` — headless, stdout/stderr separated
- **Configuration**
  - the `eonyx.json` cascade: global → data → project
  - strict schema, `$ENV` references for secrets
- **Models**
  - any OpenAI-compatible API
  - `context_window` triggers auto-summarization
  - prices — the cost counter in the header
- **Permissions**
  - reading is always allowed
  - everything else — via `allowed_tools`, the TUI dialog, `--yolo`, or `/plan`
- **Hooks**
  - `before` / `after` around tools
  - context through `EONYX_*` variables
  - `before` can block
{{< /admonition >}}

