Thank you for your interest in contributing to nsh! This guide covers how to contribute effectively, whether you're fixing a typo, adding a feature, or submitting an AI-generated patch.
Be respectful, constructive, and patient. We're building a tool together.
- Fork the repository and clone your fork
- Set up the development environment:
# Rust 1.85+ required rustup update cargo build cargo test
- Run the quality pipeline before submitting:
This runs format checking, clippy lints (warnings as errors), all tests, and a dependency audit.
cargo make quality
- Search existing issues first to avoid duplicates
- Use a clear title that summarizes the problem
- Include reproduction steps - shell type, OS, nsh version (
nsh --version), relevant config - Include logs - run with
RUST_LOG=debugand attach relevant output - Describe expected vs. actual behavior
- Open a discussion or issue before building large features
- Explain the use case - what problem does this solve?
- Consider how it fits into nsh's design: local-first, secure by default, non-intrusive
- For non-trivial changes, open an issue first to discuss the approach
- Keep PRs focused on a single logical change
- If a PR needs "and" in its title, consider splitting it
- All tests pass:
cargo test - No lint warnings:
cargo clippy --all-targets -- -D warnings - Formatted code:
cargo fmt -- --check - Commit messages are clear, single-line summaries of what changed:
- ✅
Add entity-aware history search for hostnames and IPs - ✅
Fix FTS5 index corruption on concurrent access - ✅
Update default model chain to include gemini-3-flash - ❌
Fix stuff - ❌
WIP changes part 3
- ✅
- New features include tests - look at existing tests for patterns
- Documentation is updated if the change affects user-facing behavior
- Fork → branch → commit → push → open PR against
main - Fill in the PR template (if one exists) or describe:
- What the change does
- Why it's needed
- How to test it
- A maintainer will review and may request changes
- Once approved, the PR will be squash-merged
We welcome AI-generated contributions. Use whatever tools help you be productive - Copilot, Claude, Cursor, Amp, or anything else.
Important: AI-generated PRs are held to the same quality bar as any other contribution. They must pass all tests, follow the code style, and solve a real problem. Human-authored PRs will be prioritized in the review queue over automated or bulk-generated ones.
If your PR was substantially AI-generated, please mention this in the PR description. This isn't a penalty - it helps reviewers calibrate their review (AI-generated code sometimes has subtle issues that benefit from closer inspection).
Low-effort, bulk-generated PRs (e.g., mass-renaming variables, adding trivial comments, reformatting code that already passes cargo fmt) will be closed without review.
Understanding the codebase helps you contribute effectively:
src/
├── shim_main.rs # Shim entry point (stable, handles `nsh wrap`, execs nsh-core)
├── core_main.rs # Core entry point (calls lib::main_inner)
├── lib.rs # Shared main_inner entry point and modules
├── cli.rs # Clap argument definitions
├── config.rs # Configuration loading, merging, validation
├── context.rs # Query context building (env, project, git, scrollback)
├── query.rs # Main query handler, agent loop, tool dispatch
├── db.rs # SQLite database (schema, queries, FTS5)
├── provider/ # LLM provider implementations
│ ├── mod.rs # Unified message types, provider factory
│ ├── anthropic.rs # Anthropic API (native)
│ ├── openai_compat.rs # OpenAI-compatible API (shared by OpenRouter, OpenAI, Gemini, Ollama)
│ └── chain.rs # Model chain with retry and fallback
├── tools/ # Tool implementations
│ ├── mod.rs # Tool definitions and path validation
│ ├── command.rs # Command prefill with risk assessment
│ ├── chat.rs # Text response rendering
│ ├── search_history.rs # FTS5 + entity-aware history search
│ ├── write_file.rs / patch_file.rs # File editing with diff preview
│ └── ... # Other tools
├── pump.rs # PTY I/O pump, scrollback capture (stable shim boundary)
├── pty.rs # PTY creation, raw mode, fork/exec (stable shim boundary)
├── daemon.rs # Daemon protocol, DB command thread
├── global_daemon.rs # Global daemon: SIGHUP drain+reexec, multi-threaded workers
├── redact.rs # Secret detection and redaction engine
├── security.rs # Command risk assessment, injection filtering
├── mcp.rs # MCP client (stdio + HTTP transports)
├── skills.rs # Custom skill loader and executor
├── streaming.rs # Stream consumer, spinner, display
├── summary.rs # Command output summarization
└── shell/ # Shell integration scripts (zsh, bash, fish, powershell)
- Shim/Core split -
nshis a frozen shim; it handleswrapdirectly and otherwise execs the latestnsh-core. Updates never require restarting terminals. - PTY wrapper -
nsh wrapruns the user's shell inside a PTY so nsh can capture all terminal output without modifying the shell itself. PTY code is part of the shim and changes extremely rarely. - Global daemon - The background daemon handles writes, reads, and memory processing. On updates it receives SIGHUP, drains current connections (with timeout), then re-execs to the latest core binary.
- Daemon thread - DB writes and command recording happen on a dedicated thread via
daemon.rsto avoid blocking the shell - Tool-call-only responses - the LLM must always respond via tool calls, never plain text. This ensures structured, actionable responses
- Boundary tokens - tool results are wrapped in random boundary tokens to prevent prompt injection from tool output
- Schema migrations -
db.rshandles schema versioning (currently v5) with backward-compatible migrations
- Follow existing patterns in the codebase
- Use
anyhowfor error handling in application code - Use
rusqlite::Resultin database code - Prefer
tracing::debug!/tracing::warn!overeprintln!for internal diagnostics - User-facing output goes to stderr (
eprintln!), not stdout - Tests go in
#[cfg(test)] mod testsat the bottom of each file
The test suite is substantial. To run specific subsets:
cargo test # all tests
cargo test --lib # unit tests only
cargo test --test integration # integration tests only
cargo test db::tests # tests in a specific module
cargo test -- --nocapture # show test outputWhen adding tests:
- Use
Db::open_in_memory()for database tests - Use
tempfile::TempDirfor filesystem tests - Use
wiremockfor HTTP mocking (provider tests) - Use
#[serial_test::serial]for tests that modify environment variables
Open an issue or start a discussion. We're happy to help you find the right place to contribute.