narwhal
Health Gecti
- License — License: NOASSERTION
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 10 GitHub stars
Code Gecti
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
TUI database client with a built-in MCP server. Five databases (postgres, mysql, sqlite, duckdb, clickhouse), vim editing, Lua plugins.
narwhal
A TUI database client with a built-in MCP server. Five databases, vim editing, Lua plugins.

Why narwhal
- Built-in MCP server. Run
narwhal mcpand any
Model Context Protocol client (Claude
Desktop, Cursor, your own agent) getslist_connections,describe_schema,describe_table,run_query,explain_queryover
stdio. Read-only by default, with a three-layer SQL guard and a
workspace ACL (.narwhal/workspace.toml) so an agent can only see
the connections you explicitly listed. - One TUI, five databases. Postgres, MySQL, SQLite, DuckDB,
ClickHouse. No driver-juggling, no context-switching betweenpsql,mysql, and DataGrip. - Vim editing + auto-pair + completion. Modal input (Normal,
Insert, Visual), schema-aware tab-completion, alias-resolved column
hints, a proper:command palette. - Lua plugin runtime. The bits that should be yours, stay yours.
Write a.luafile, drop it in~/.config/narwhal/plugins/, and it
is live. - SSH tunnels,
~/.pgpass, OS keyring. The auth ergonomics you
already configured forpsqlwork here too. Setssh_host=jump.example.comand the connect path forwards a loopback
port for you.
Install
Cargo (any platform with Rust)
cargo install narwhaldb
The crates.io name is
narwhaldb(the barenarwhalslot is held
by an unrelated 2018 docker library); the installed binary is still
justnarwhal.
For users without a Rust toolchain, cargo-binstall fetches the
prebuilt binary instead of compiling:
cargo binstall narwhaldb
Homebrew (macOS, Linuxbrew)
brew tap Nonanti/tap
brew install narwhal
Arch Linux (AUR)
yay -S narwhal # or: paru -S narwhal
Nix
nix run github:Nonanti/narwhal
Or add the flake to your inputs and reference the default package.
Pre-built binaries
Download native tarballs (with SHA-256 checksums) from the
latest GitHub Release:
x86_64-unknown-linux-gnuaarch64-apple-darwin(Apple Silicon)
Intel Mac users: build from source with cargo install narwhaldb for
now; a prebuilt x86_64-apple-darwin tarball is on the roadmap once
the macos-13 runner backlog clears.
curl -LO https://github.com/Nonanti/narwhal/releases/latest/download/narwhal-1.0.0-x86_64-unknown-linux-gnu.tar.gz
tar -xzf narwhal-1.0.0-*.tar.gz
mv narwhal-1.0.0-*/narwhal ~/.local/bin/
Build from source
git clone https://github.com/Nonanti/narwhal.git
cd narwhal
cargo build --release
# binary at target/release/narwhal
Quick start
- Run
narwhal. The TUI opens with an empty editor and a sidebar. - Hit
:add. The connection wizard appears. Pick a driver, fill in host + database (or use:url postgres://user:pass@host/dbto skip the form). :open <name>. The saved entry connects; the sidebar fills with schemas and tables.- F6 to run. The whole buffer executes; results appear in the lower pane. Press F1 any time for the full keymap reference.
connections.toml schema
Named connections live in ~/.config/narwhal/connections.toml. One[[connection]] block per database; [connection.params] carries the
driver-specific options. The field names match theConnectionParams struct —
in particular username (not user) is the canonical name.
# Local SQLite — the file path is the only required param.
[[connection]]
id = "00000000-0000-0000-0000-000000000001"
name = "smoke"
driver = "sqlite"
[connection.params]
path = "/tmp/narwhal-smoke.db"
# Postgres on a non-default port, no TLS — typical local docker setup.
[[connection]]
id = "00000000-0000-0000-0000-000000000002"
name = "demo-pg"
driver = "postgres"
[connection.params]
host = "127.0.0.1"
port = 5433
username = "postgres" # NOTE: `username`, not `user`
password = "narwhal"
database = "demo"
ssl_mode = "disable" # disable | prefer (default) | require | verify-ca | verify-full
File-local drivers (sqlite, duckdb) tolerate the default prefer
so pre-TLS configs still load; the wire layer ignores it. Network
drivers (postgres, mysql, clickhouse) accept any of the fivessl_mode values plus optional ssl_root_cert, ssl_cert, ssl_key
paths for mutual TLS.
config.toml (settings)
A ~/.config/narwhal/config.toml lets you override the renderer
theme and a few editor toggles. Missing fields fall back to their
defaults so a one-line file is enough.
theme = "dark" # "dark" (default) | "light" | "high-contrast"
[editor]
tab_width = 4 # reserved, v1.1 will honour this
use_spaces = true # reserved, v1.1 will honour this
line_numbers = true # reserved, v1.1 will honour this
[keybindings]
vim_mode = true # reserved, v1.1 will allow opt-out
v1.0 wires only the theme field; the rest are persisted and
load-validated so the file stays stable across upgrades. The renderer
warns at start-up if the file is malformed instead of silently
falling back to defaults.
SSH tunnels
Any network connection can prepend an SSH local-port-forward by
adding ssh fields to the params block. The forward is opened
implicitly on :open and torn down when the session closes.
[connection.params]
host = "db.internal" # resolved on the bastion side
port = 5432
username = "alice"
database = "prod"
[connection.params.ssh]
host = "bastion.example.com"
user = "alice"
port = 22 # optional — defaults to 22
# key_path = "~/.ssh/id_ed25519" # optional — defaults to ssh-agent / ~/.ssh/config
# jump_host = "jump.example.com" # optional — maps to `ssh -J`
The spawned ssh subprocess inherits ~/.ssh/config, the agent,Match blocks, IdentityAgent, and FIDO2 keys for free — narwhal
deliberately shells out to OpenSSH rather than embedding its own
client. URL form: ?ssh_host=bastion&ssh_user=alice on a:url postgres://... invocation.
TLS defaults changed (v0.2)
Breaking change: ssl_mode = prefer and ssl_mode = require now
perform full CA chain verification instead of accepting any server
certificate. Self-signed certificates will be rejected unless the CA
is explicitly trusted via ssl_root_cert.
If you were relying on the previous insecure behaviour:
- Self-signed servers: add
ssl_root_cert = "/path/to/ca.pem"to
the connection params, or setssl_mode = "disable"if TLS is not
needed. - Hostname mismatch: use
ssl_mode = "require"orssl_mode = "verify-ca"(chain verified, hostname skipped). - Full verification:
ssl_mode = "verify-full"(unchanged).
Query-string TLS params (?sslmode=..., ?sslrootcert=..., etc.)
are now parsed into dedicated struct fields instead of being left in
the generic options map.
MCP server: talk to your databases through an AI agent
narwhal ships a built-in Model Context Protocol
server so any MCP-capable AI assistant (Claude Desktop, Cursor, Continue,
Aider, ...) can browse the connections you already configured and inspect
their schema.
narwhal mcp # runs the JSON-RPC stdio server
Wire it into Claude Desktop:
// ~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"narwhal": {
"command": "narwhal",
"args": ["mcp"]
}
}
}
The v0 tool surface:
| Tool | What it does |
|---|---|
list_connections |
List configured connections — driver, target, SSH flag. No IO, no credentials loaded. Honours the workspace ACL. |
describe_schema |
Schema / table / view tree for one connection. |
describe_table |
Full structure of one table — columns, indexes, foreign keys, unique constraints, engine-native DDL. |
run_query |
Execute a single statement. Read-only by default — syntactic guard + BEGIN/ROLLBACK sandwich + row limit (default 1 000). read_only=false opts out, subject to the workspace ACL. |
explain_query |
Driver-native EXPLAIN with the right dialect prefix. Optional analyze=true runs the statement for real cardinalities (PG / MySQL / DuckDB). |
Every database-touching call is audit-logged to~/.local/share/narwhal/history.jsonl with source: "mcp" so you canjq 'select(.source == "mcp")' to isolate agent traffic.
Workspace scoping: .narwhal/workspace.toml
A repo-local file (discovered by walking up from pwd, same idiom as.git) declares what the MCP server may expose when narwhal runs from
inside that directory tree. Commit it next to your code so an agent
launched against your project can only reach the databases you list.
# .narwhal/workspace.toml
# Connection names from connections.toml that the agent may target.
# Empty / omitted = all of them.
allowed_connections = ["staging", "test"]
# When false, run_query rejects read_only=false. Default true.
allow_writes = false
Disallowed connections appear to the agent exactly as a misspelled
name would: the list_connections result hides them, describe_* /run_query calls answer with the same "unknown connection" tool-level
error, and the agent retries against the visible set automatically.
Keymap
Global
| Keys | Action |
|---|---|
| F5 / Alt-Enter / Ctrl-; | Run statement under cursor |
| F6 | Run whole buffer |
| F7 | Stream cursor statement |
| F4 / Ctrl-C | Cancel running query |
| Ctrl-W | Cycle pane focus |
| Ctrl-T | New editor tab |
| Ctrl-Tab / Ctrl-Shift-Tab | Cycle tabs |
| ? / F1 | Help |
| :q | Quit |
| :refresh | Re-fetch schema tree for active connection |
Editor
| Keys | Action |
|---|---|
| i / a | Enter insert mode |
| Esc | Back to normal mode |
| Tab / Ctrl-Space | Completion |
| ↑ ↓ / Shift-Tab | Cycle popup items |
| Enter / Tab (in popup) | Accept completion |
| h j k l / arrows | Move cursor |
| w / b | Word forward / backward |
| 0 / $ | Line start / end |
| v / V | Visual / visual-line mode |
Sidebar
| Keys | Action |
|---|---|
| j / k / ↑ / ↓ | Navigate |
| Enter | Describe table |
| o | Preview table data |
| d | Inject DDL into editor |
Results
| Keys | Action |
|---|---|
| h j k l / arrows | Move selection |
| Enter | Open cell popup |
| e | Edit cell value |
| y / Y | Yank cell / row to clipboard |
| / | Filter rows |
| n / N | Next / prev search match |
| g / G | Jump to first / last row |
| :next / :prev | Page through results |
Snippets
| Keys | Action |
|---|---|
| :save <name> | Save editor buffer as a named snippet |
| :load <name> | Load a snippet into a new tab |
| :rm-snippet <name> | Delete a saved snippet |
| :snippets | Browse saved snippets |
Plugins
Plugins are Lua scripts that auto-load from ~/.config/narwhal/plugins/*.lua
(or the platform equivalent under $XDG_CONFIG_HOME). They get a narwhal
global with these entry points:
narwhal.register_command(name, description, handler)
-- handler(arg : string)
-- return "..." -> status bar message
-- return { sql = "..." } -> append to editor buffer
-- return { sql = "...", append = false }
-- -> replace editor buffer
-- return nil | false -> silent
narwhal.register_transform(handler)
-- handler(result : table)
-- mutate in place; return value ignored
narwhal.sql_run(sql : string) -> result
-- Run SQL on the active connection synchronously
narwhal.editor_text : string (read-only)
-- Current editor buffer content during command dispatch
Sample plugins
Six working samples live in examples/plugins/:
| File | What it does |
|---|---|
uppercase.lua |
Result transform that uppercases every TEXT cell |
format_json.lua |
Pretty-prints cells that parse as JSON |
row_count.lua |
:rc <table> — count rows via narwhal.sql_run |
query_snippet.lua |
:top <table> — inject SELECT * FROM … LIMIT 10 |
csv_export.lua |
:csv-export <table> <path> — dump to CSV |
explain_cost.lua |
:explain-cost / :explain-sqlite — wrap buffer in EXPLAIN |
Load on demand: :plug-load /path/to/file.lua. List everything: :plug-list.
For the full API reference, see narwhal-plugin-lua docs
and the plugin examples README.
Security model
Plugins are trusted code that runs with your privileges. They can run
arbitrary SQL, inject into the editor, and read every result row. Only
install scripts from sources you'd trust as a shell script. There is no
sandbox — by design, so auditing a plugin is just reading a short .lua
file.
Built-in command names (run, open, begin, quit, …) are reserved;
a plugin that tries to shadow one is rejected at load time. During a:begin transaction, narwhal.sql_run is refused entirely.
Headless exec mode: one-shot SQL from the shell
The same binary doubles as a psql -c / mysql -e muadili. Use it in
CI smoke checks, cron jobs, or shell pipelines:
narwhal exec --conn prod 'SELECT count(*) FROM users'
# Choose a format for the consumer (default: table)
narwhal exec -c prod -f json 'SELECT id, email FROM users' | jq '.[].email'
narwhal exec -c prod -f csv 'SELECT * FROM orders' > orders.csv
narwhal exec -c prod -f tsv 'SELECT id, name FROM users' | column -t
# Limit rows to keep large queries snappy
narwhal exec -c prod -l 100 'SELECT * FROM events'
# Writes are sandboxed by default (BEGIN ... ROLLBACK). `--write` opts out:
narwhal exec -c prod --write "UPDATE users SET banned = true WHERE id = 42"
Formats: table (default, ASCII grid), csv (RFC 4180), json
(array-of-objects), tsv (pipe-friendly, no quoting). Connection
resolution is the same as the TUI — keyring first, ~/.pgpass/env
fallback. Every call is audit-logged to history.jsonl withsource: "exec" so it can be grepped alongside MCP traffic.
Transactions
| Command | Action |
|---|---|
:begin [iso] |
Open a pinned connection; iso accepts ru/rc/rr/s short forms |
:commit |
Commit and close the pinned connection |
:rollback |
Rollback and close the pinned connection |
:savepoint NAME |
Create a named savepoint (drivers that support them) |
:release NAME |
Release a savepoint |
:rollback-to NAME |
Rollback to a savepoint |
A TX badge on the status bar reminds you that you're inside a
transaction.
Architecture
narwhal (bin)
│ entry point + CLI
▼
narwhal-app narwhal-mcp
orchestrator MCP server
│ │ │ │
▼ ▼ ▼ ▼
narwhal-tui narwhal-commands narwhal-driver-registry
render completion / export / (feature-gated
wizard / dispatch / driver lookup)
snippets / DDL / … │
│ │ ▼
└─────────┼────────────────┐ narwhal-driver-*
▼ │ postgres · sqlite
narwhal-domain │ mysql · duckdb
pure model state │ clickhouse
(editor buffer, schema │
listings, no IO, │
no rendering) │
│ ▼
└────────► narwhal-pool · narwhal-sql ·
narwhal-history · narwhal-config ·
narwhal-vim
│
▼
narwhal-core
driver trait, value model,
schema types, error type
Dependency direction: every arrow points downward. View (narwhal-tui)
and commands (narwhal-commands) read domain state by reference; onlynarwhal-app is allowed to mutate it in response to user input.
Concrete drivers are reached exclusively throughnarwhal-driver-registry, so cargo features are the only place where
database engines come and go.
For the full layer map see docs/ARCHITECTURE.md;
for the code-style contract see docs/STYLE.md.
Safety
- Every destructive operation goes through the
:command line, never a hotkey. - Statements are journaled to
~/.local/share/narwhal/history.jsonlbefore execution. - Passwords prefer the OS keyring; an in-memory fallback is used only when the keyring isn't available.
:forget <name>wipes the cached entry.
Building
Nix
nix develop
cargo build --release
The dev shell pulls in cmake, clang, and libcxx for the bundled
DuckDB C++ build, and pre-sets LIBCLANG_PATH for bindgen.
Other systems
Any toolchain at or above the version pinned in rust-toolchain.toml,
plus the usual native build deps for DuckDB (cmake, a C++17 compiler).
cargo build --release
Benchmarks
Criterion harnesses live under crates/*/benches/. Run them all withcargo bench --workspace or one at a time:
cargo bench -p narwhal-sql --bench splitter
cargo bench -p narwhal-tui --bench sort
cargo bench -p narwhal-tui --bench editor_motion
cargo bench -p narwhal-history --bench append
The pre/post-optimisation numbers are recorded indocs/perf-after-phase-2.md; current
headline numbers on a Linux box are ~900 MiB/s for the statement
splitter, ~38 µs for a 5 000-line w motion, and ~1.15 ms to sort
2 000 JSON cells.
Contributing
A few ground rules so PRs land smoothly:
cargo fmt --all,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace, andRUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-depsall pass in CI; please run them locally too.- New behaviour ships with a regression test under the relevant
crate'stests/directory. - Commit messages follow Conventional Commits
(feat:,fix:,refactor:,docs:,chore:).
Licence
Dual-licensed under the MIT and
Apache 2.0 licences. Contributions are accepted under
the same terms.
See docs/RELEASING.md for the release
checklist and CHANGELOG.md for the version history.
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi