Skip to content

Commit 4b616e8

Browse files
authored
Add Nushell Agent Skill (#1231)
This is the skill I use for working with Nushell in my agent TUIs. It's been tested to identify the most common issues I've come across while using Nushell with LLMs, and so far in testing has all but eliminated failing tool calls due to the model misunderstanding how to use Nushell.
1 parent 2896246 commit 4b616e8

File tree

8 files changed

+1027
-0
lines changed

8 files changed

+1027
-0
lines changed

skills/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Agent Skills
2+
3+
This directory holds examples for use with coding agents that interact with Nu,
4+
either as a CLI tool or via the Nushell MCP server.
5+
6+
## Nushell Skill
7+
8+
`./skills/nushell/` - An [Agent Skills](https://agentskills.io/home) compliant
9+
skill directory built via the Claude Code skill-writer superpower and human-tested
10+
for validity and efficacy. Once added to the appropriate location, begin a new
11+
session with your coding agent and it should automatically register the skill
12+
and use it when you reference Nushell, mention the skill directly, or trigger a
13+
use case mentioned in the description of the SKILL.md frontmatter.
14+
15+
### Global usage
16+
17+
Copy the `nushell/` directory to `~/.claude/skills/` for global use in Claude Code,
18+
to `~/.copilot/skills` for global use with the Copilot coding agent or GitHub
19+
Copilot CLI, or to `~/.agent/skills/` for Codex, OpenCode, and other coding agents.
20+
See your preferred LLM provider's documentation for specific details.
21+
22+
### Project usage
23+
24+
If you only wish to load this skill for specific projects, or for use with non-
25+
CLI based coding agents that canot access a global skills directory, create a
26+
`.claude/skills/`, `.github/skills/`, or `.agents/skills/` directory at the root
27+
of your project and copy the nushell skill directory into it.

skills/nushell/SKILL.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
name: nushell
3+
description: Use when writing or running Nushell commands, scripts, or pipelines - via the Nushell MCP server (mcp__nushell__evaluate), via Bash (nu -c), or in .nu script files. Also use when working with structured data (JSON, YAML, TOML, CSV, Parquet, SQLite), doing ad-hoc data analysis or exploration, or when the user's shell is Nushell.
4+
---
5+
6+
# Using Nushell
7+
8+
Nushell is a structured-data shell. Commands pass **tables, records, and lists** through pipelines - not text.
9+
10+
**Two execution paths:**
11+
- **MCP server**: `mcp__nushell__evaluate` - persistent REPL (variables survive across calls)
12+
- **Bash tool**: `nu -c '<code>'` - one-shot (use single quotes for outer wrapper)
13+
14+
## Critical Rules
15+
16+
**NEVER use bare `print` in MCP stdio mode.** Output will be lost (returns empty `[]`). Use `print -e "msg"` for stderr, or just return the value (implicit return).
17+
18+
**String interpolation uses parentheses, NOT curly braces:**
19+
```nu
20+
# WRONG: $"hello {$name}"
21+
# CORRECT: $"hello ($name)"
22+
$"($env.HOME)/docs" $"2 + 2 = (2 + 2)" $"files: (ls | length)"
23+
```
24+
Gotcha: `$"(some text)"` errors - parens are evaluated as code. Escape literal parens: `\(text\)`.
25+
26+
**No bash syntax:** `cmd1; cmd2` not `&&`, `o+e>|` not `2>&1`, `$env.VAR` not `$VAR`, `(cmd)` not `$(cmd)`.
27+
28+
## Common Mistakes
29+
30+
| Mistake | Fix |
31+
|---------|-----|
32+
| `$"hello {$name}"` | `$"hello ($name)"` |
33+
| `print "msg"` in MCP | `print -e "msg"` or return value |
34+
| `command 2>&1` | `command o+e>\| ...` |
35+
| `$HOME/path` | `$env.HOME` or `$"($env.HOME)/path"` |
36+
| `export FOO=bar` | `$env.FOO = "bar"` |
37+
| Mutating in closure | Use `reduce`, `generate`, or `each` |
38+
| `\u001b` for ANSI | `ansi strip` to remove, `char --unicode '1b'` for ESC |
39+
| `where ($in.a > 1) and ($in.b > 2)` | Second `$in` rebinds to bool. Use bare cols: `where a > 1 and b > 2` |
40+
| `where not ($in.col \| cmd)` | `not` breaks `$in`. Use `where ($in.col \| cmd) == false` |
41+
| `where col \| cmd` (no parens) | Parsed as two pipeline stages. Use `where ($in.col \| cmd)` |
42+
43+
## When to Use Nushell
44+
45+
**Always prefer Nushell for:**
46+
- Any structured data (JSON, YAML, TOML, CSV, Parquet, SQLite) - unifies all formats
47+
- CLI tools with `--json` flags - pipe JSON output directly into Nushell for querying (e.g. `^gh pr list --json title,state | from json`)
48+
- Ad-hoc data analysis and exploration - faster than Python setup
49+
- Initial data science/analytics - histograms, tabular output, basic aggregations
50+
- Polars plugin for large datasets - DataFrames without Python overhead
51+
52+
**Use Bash only when:** bash-specific tooling, MCP unavailable, or bash-syntax integrations.
53+
54+
## Reference Files
55+
56+
Read the relevant file(s) based on what you need:
57+
58+
| File | Read when you need... |
59+
|------|-----------------------|
60+
| [commands.md](references/commands.md) | Command reference tables (filters, strings, conversions, filesystem, math, dates) |
61+
| [types-and-syntax.md](references/types-and-syntax.md) | Type system, string types, operators, variables, control flow, cell-paths |
62+
| [data-analysis.md](references/data-analysis.md) | Format conversion, HTTP, Polars, SQLite, aggregation patterns |
63+
| [advanced.md](references/advanced.md) | Custom commands, modules, error handling, jobs, external commands, env config |
64+
| [bash-equivalents.md](references/bash-equivalents.md) | Complete Bash-to-Nushell translation table |
65+
| [http-transport.md](references/http-transport.md) | Differences when using HTTP transport instead of stdio |
66+
67+
## MCP Server Quick Notes
68+
69+
- `mcp__nushell__evaluate` - run code; `mcp__nushell__list_commands` - discover; `mcp__nushell__command_help` - help
70+
- State persists between calls. `$history` stores prior results (access `$history.0`, etc.)
71+
- Use `| to text` or `| to json` for large outputs. Use `ansi strip` for color removal.
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Nushell Advanced Patterns
2+
3+
## Custom Commands with Full Signatures
4+
5+
```nu
6+
def search [
7+
pattern: string # required positional
8+
path?: string # optional positional
9+
--case-sensitive(-c) # boolean flag (defaults false)
10+
--max-depth(-d): int # flag with value
11+
...extra: string # rest params
12+
]: nothing -> list<string> {
13+
# implementation
14+
}
15+
```
16+
17+
### Pipeline Input/Output Types
18+
```nu
19+
def "str stats" []: string -> record {
20+
let input = $in
21+
{length: ($input | str length), words: ($input | split words | length)}
22+
}
23+
24+
# Multiple input types
25+
def process []: [string -> record, list -> table] { ... }
26+
```
27+
28+
### Environment-Modifying Commands
29+
```nu
30+
def --env goto [dir: string] {
31+
cd $dir
32+
$env.LAST_DIR = (pwd)
33+
}
34+
```
35+
36+
### Wrapped External Commands
37+
```nu
38+
def --wrapped mygrep [...rest] {
39+
^grep --color=always ...$rest
40+
}
41+
```
42+
43+
## Modules
44+
45+
```nu
46+
module utils {
47+
export def double [x: number] { $x * 2 }
48+
export def triple [x: number] { $x * 3 }
49+
export-env { $env.UTILS_LOADED = true }
50+
}
51+
52+
use utils double # import specific command
53+
use utils * # import all exports
54+
use utils [] # import only environment
55+
```
56+
57+
## Generate (Stateful Sequences)
58+
59+
Closure comes first, initial value second. Returns `{out: value, next: state}`. Omit `next` to stop.
60+
61+
```nu
62+
# Fibonacci
63+
generate {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]}} [0, 1] | first 10
64+
65+
# Counter
66+
generate {|i| if $i <= 5 { {out: $i, next: ($i + 1)} }} 0
67+
68+
# With input stream (two-arg closure)
69+
1..5 | generate {|e, sum=0| let sum = $e + $sum; {out: $sum, next: $sum}}
70+
```
71+
72+
## Functional Alternatives to Mutable State
73+
74+
```nu
75+
# Instead of: mut sum = 0; for x in list { $sum += $x }
76+
[1 2 3 4 5] | reduce {|it, acc| $acc + $it}
77+
78+
# With initial value
79+
[2 3 4 5] | reduce --fold 1 {|it, acc| $acc * $it}
80+
81+
# Instead of: mut results = []; for x in list { $results = ($results | append (f $x)) }
82+
$list | each {|x| process $x}
83+
84+
# Instead of: while with mutation
85+
generate {|state| if $state.done { null } else { {out: $state.val, next: (advance $state)} }} $initial
86+
```
87+
88+
## Error Handling
89+
90+
```nu
91+
# Basic try/catch
92+
try { open nonexistent.json } catch {|e| $"Failed: ($e.msg)" }
93+
94+
# With finally (v0.111.0+)
95+
try { do-work } catch {|e| log-error $e.msg } finally { cleanup }
96+
97+
# Custom errors
98+
error make {msg: "Value must be non-negative"}
99+
error make {msg: "Bad value", label: {text: "here", span: (metadata $value).span}}
100+
```
101+
102+
## External Commands
103+
104+
```nu
105+
^external_cmd args # explicit external invocation
106+
ls | to text | ^grep pattern # pipe structured data to external
107+
^cmd arg1 arg2 o+e>| str trim # capture stdout+stderr
108+
^cmd | complete # get {exit_code, stdout, stderr} record
109+
$env.LAST_EXIT_CODE # check last exit code
110+
111+
# External output to structured data
112+
^git log --oneline -5 | lines | parse "{hash} {message}"
113+
114+
# Pipe structured data to external
115+
{data: "value"} | to json | ^curl -X POST -d @- https://api.example.com
116+
```
117+
118+
## Background Jobs
119+
120+
```nu
121+
job spawn { long_running_cmd } # returns job ID
122+
job spawn --tag "server" { uvicorn main:app } # with description
123+
job list # list running jobs
124+
job kill $id # terminate job
125+
126+
# Getting results back via mailbox (main thread = job 0)
127+
job spawn { expensive_calc | job send 0 }; job recv
128+
job spawn { cmd | job send 0 --tag 1 }; job recv --tag 1 # filtered
129+
job recv --timeout 5sec # with timeout
130+
```
131+
132+
**Key rules:** `job recv` reads from current job's mailbox only (no job ID arg). Background jobs send to main with `job send 0`. Use `--tag` for filtering.
133+
134+
## Environment & Configuration
135+
136+
```nu
137+
$env.PATH # PATH (list with ENV_CONVERSIONS, string otherwise)
138+
$env.Path # PATH on Windows
139+
$env.PATH = ($env.PATH | append "/new/path")
140+
$env.MY_VAR = "value"
141+
$env.MY_VAR? | default "fallback" # safe access with default
142+
hide-env MY_VAR # unset variable
143+
144+
# Configuration
145+
$env.config.show_banner = false # set individual config values
146+
$nu.default-config-dir # config directory path
147+
$nu.home-dir # home directory
148+
config nu # edit config in $EDITOR
149+
```
150+
151+
## Using nu -c from Bash
152+
153+
```bash
154+
# Simple command
155+
nu -c 'ls | where size > 1mb | to json'
156+
157+
# String interpolation (outer single quotes to avoid $ conflicts)
158+
nu -c 'let x = 42; $"answer: ($x)"'
159+
160+
# Multi-statement
161+
nu -c 'let data = (open file.json); $data | get field'
162+
```
163+
164+
Use single quotes for the outer wrapper since Nushell uses `$` for variables.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Bash to Nushell Translation
2+
3+
## File and Directory Operations
4+
5+
| Bash | Nushell |
6+
|------|---------|
7+
| `ls` | `ls` |
8+
| `ls -la` | `ls --long --all` or `ls -la` |
9+
| `ls -d */` | `ls \| where type == dir` |
10+
| `find . -name "*.rs"` | `glob **/*.rs` or `ls **/*.rs` |
11+
| `mkdir -p path` | `mkdir path` (creates parents automatically) |
12+
| `touch file` | `touch file` |
13+
| `cp src dst` | `cp src dst` |
14+
| `cp -r src dst` | `cp -r src dst` |
15+
| `mv old new` | `mv old new` |
16+
| `rm file` | `rm file` |
17+
| `rm -rf dir` | `rm -r dir` |
18+
| `cat file` | `open --raw file` |
19+
| `head -5 file` | `open --raw file \| lines \| first 5` |
20+
| `tail -5 file` | `open --raw file \| lines \| last 5` |
21+
| `wc -l file` | `open --raw file \| lines \| length` |
22+
23+
## Output Redirection
24+
25+
| Bash | Nushell |
26+
|------|---------|
27+
| `> file` | `o> file` or `\| save file` |
28+
| `>> file` | `o>> file` or `\| save --append file` |
29+
| `> /dev/null` | `\| ignore` |
30+
| `> /dev/null 2>&1` | `o+e>\| ignore` |
31+
| `2>&1` | `o+e>\| ...` |
32+
| `cmd \| tee log \| cmd2` | `cmd \| tee { save log } \| cmd2` |
33+
34+
## Text Processing
35+
36+
| Bash | Nushell |
37+
|------|---------|
38+
| `grep pattern` | `where $it =~ pattern` or `find pattern` |
39+
| `grep -r pattern .` | `open --raw file \| lines \| where $it =~ pattern` |
40+
| `sed 's/old/new/'` | `str replace 'old' 'new'` |
41+
| `sed 's/old/new/g'` | `str replace --all 'old' 'new'` |
42+
| `awk '{print $1}'` | `split column ' ' \| get column0` |
43+
| `sort` | `sort` |
44+
| `sort -u` | `sort \| uniq` |
45+
| `uniq` | `uniq` |
46+
| `cut -d: -f1` | `split column ':' \| get column0` |
47+
| `tr '[:upper:]' '[:lower:]'` | `str downcase` |
48+
49+
## Variables and Environment
50+
51+
| Bash | Nushell |
52+
|------|---------|
53+
| `VAR=value` | `let var = "value"` |
54+
| `export VAR=value` | `$env.VAR = "value"` |
55+
| `echo $VAR` | `$env.VAR` |
56+
| `echo $HOME` | `$env.HOME` |
57+
| `echo $PATH` | `$env.PATH` (list on macOS/Linux) |
58+
| `echo $?` | `$env.LAST_EXIT_CODE` |
59+
| `unset VAR` | `hide-env VAR` |
60+
| `${VAR:-default}` | `$env.VAR? \| default "value"` |
61+
| `export PATH=$PATH:/new` | `$env.PATH = ($env.PATH \| append "/new")` |
62+
| `FOO=bar cmd` | `FOO=bar cmd` (same syntax) |
63+
64+
## Command Execution
65+
66+
| Bash | Nushell |
67+
|------|---------|
68+
| `cmd1 && cmd2` | `cmd1; cmd2` |
69+
| `cmd1 \|\| cmd2` | `try { cmd1 } catch { cmd2 }` |
70+
| `$(command)` | `(command)` |
71+
| `` `command` `` | `(command)` |
72+
| `command &` | `job spawn { command }` |
73+
| `jobs` | `job list` |
74+
| `kill %1` | `job kill $id` |
75+
| `type cmd` | `which cmd` |
76+
| `man cmd` | `help cmd` |
77+
| `bash -c 'code'` | `nu -c 'code'` |
78+
| `source script.sh` | `source script.nu` |
79+
80+
## Control Flow
81+
82+
| Bash | Nushell |
83+
|------|---------|
84+
| `if [ cond ]; then ... fi` | `if $cond { ... }` |
85+
| `[ -f file ]` | `"file" \| path exists` |
86+
| `[ -d dir ]` | `("dir" \| path exists) and ("dir" \| path type) == "dir"` |
87+
| `for f in *.md; do echo $f; done` | `ls *.md \| each { $in.name }` |
88+
| `for i in $(seq 1 10); do ...; done` | `for i in 1..10 { ... }` |
89+
| `while read line; do ...; done < file` | `open file \| lines \| each {\|line\| ... }` |
90+
| `case $x in ... esac` | `match $x { pattern => result, ... }` |
91+
92+
## Pipes and Subshells
93+
94+
| Bash | Nushell |
95+
|------|---------|
96+
| `cmd \| head -5` | `cmd \| first 5` |
97+
| `cmd \| tail -5` | `cmd \| last 5` |
98+
| `\` (line continuation) | `(` wrap in parens `)` |
99+
| `read var` | `let var = (input)` |
100+
| `read -s secret` | `let secret = (input -s)` |
101+
102+
## History
103+
104+
| Bash | Nushell |
105+
|------|---------|
106+
| `!!` | `!!` (inserts, doesn't execute) |
107+
| `!$` | `!$` |
108+
| `Ctrl+R` | `Ctrl+R` |
109+
110+
## Key Behavioral Differences
111+
112+
1. **Structured vs text**: Nushell pipelines pass tables/records, not text streams
113+
2. **Immutable by default**: `let` is immutable; use `mut` when needed
114+
3. **No word splitting**: Nushell doesn't split variables on whitespace
115+
4. **No glob expansion in variables**: Globs only expand in command position
116+
5. **Static parsing**: Code is parsed before execution; no `eval` equivalent
117+
6. **Implicit return**: Last expression is the return value; no `echo` needed
118+
7. **`>` is comparison**: Use `o>` or `save` for file redirection

0 commit comments

Comments
 (0)