LLM access via the 9P filesystem protocol.
llm9p enables users, scripts, and AI agents to interact with Large Language Models through standard filesystem operations. Write a prompt to a file, read the response from the same file. Supports multiple backends including Anthropic API, Claude Code CLI, and local LLMs (planned).
| Backend | Status | Description |
|---|---|---|
| Anthropic API | Available | Direct API access with your API key |
| Claude Code CLI | Available | Uses Claude Max subscription via claude command |
| Local LLMs (Ollama) | Planned | Run models locally without cloud dependencies |
The pluggable backend architecture makes it easy to add new LLM providers.
9P is a simple, lightweight network filesystem protocol originally developed for Plan 9 from Bell Labs. It lets you access remote resources as if they were local files. This means you can interact with LLMs using basic file operations (cat, echo, >, <) instead of SDKs or HTTP APIs.
Why use 9P for LLM access?
- Universal: Any language or tool that can read/write files can use it
- Scriptable: Chain LLM calls with standard Unix pipes and shell scripts
- Composable: Mount multiple 9P services and combine them
- Simple: No libraries, no dependencies, just files
go install github.com/NERVsystems/llm9p/cmd/llm9p@latestOr build from source:
git clone https://github.com/NERVsystems/llm9p
cd llm9p
go build -o llm9p ./cmd/llm9pOption A: Using Anthropic API (requires API key)
export ANTHROPIC_API_KEY=sk-ant-...
./llm9p -addr :5640Option B: Using Claude Max Subscription (via Claude Code CLI)
If you have a Claude Max subscription and the Claude Code CLI installed:
./llm9p -addr :5640 -backend cliThis uses your Claude Max subscription instead of API tokens. No API key required.
Requirements for CLI backend:
- Claude Code CLI installed and authenticated (
claudecommand available) - Active Claude Max subscription
There are several ways to mount the filesystem depending on your environment.
plan9port provides Plan 9 tools for Unix systems.
# Install on macOS
brew install plan9port
# Install on Debian/Ubuntu
sudo apt-get install 9base
# Mount using 9pfuse
mkdir -p /mnt/llm
9pfuse localhost:5640 /mnt/llm
# Or use the 9p tool directly (no mount needed)
9p -a localhost:5640 ls llm
9p -a localhost:5640 read llm/model
9p -a localhost:5640 write llm/ask "What is 2+2?"
9p -a localhost:5640 read llm/askInfernode is a hosted Inferno OS environment with native 9P support. This is an excellent way to explore 9P filesystems.
# Start infernode
cd /path/to/infernode
./emu
# Inside infernode shell, mount llm9p
# Note: Use IP address 127.0.0.1, not "localhost"
mkdir /n/llm
mount -A tcp!127.0.0.1!5640 /n/llm
# List available files
ls -l /n/llm
# Check current model
cat /n/llm/model
# Ask a question
echo 'What is the capital of France?' > /n/llm/ask
cat /n/llm/ask
# View conversation history
cat /n/llm/context
# Reset conversation
echo reset > /n/llm/newInfernode Tips:
- Always use
127.0.0.1instead oflocalhostfor the server address - Create the mount point with
mkdir /n/llmbefore mounting - The
-Aflag to mount enables anonymous authentication (no auth required) - Use single quotes around prompts to avoid shell interpretation issues
Linux has built-in 9P filesystem support via the 9p kernel module.
# Mount via kernel 9p module
sudo mount -t 9p -o port=5640,version=9p2000 localhost /mnt/llm# Ask a question
echo "What is 2+2?" > /mnt/llm/ask
cat /mnt/llm/ask
# View token usage
cat /mnt/llm/tokens
# Change model
echo "claude-3-haiku-20240307" > /mnt/llm/model
# Adjust temperature
echo "0.5" > /mnt/llm/temperature
# Set a system prompt (persists across conversation resets)
echo "You are a helpful coding assistant." > /mnt/llm/system
# View current system prompt
cat /mnt/llm/system
# View conversation history
cat /mnt/llm/context
# Add a system message
echo "You are a helpful coding assistant." > /mnt/llm/context
# Reset conversation
echo "" > /mnt/llm/new
# View help
cat /mnt/llm/_example/llm/
├── ask # Write prompt, read response (same file)
├── model # Read/write: current model name
├── temperature # Read/write: temperature float (0.0-2.0)
├── system # Read/write: system prompt (persists across resets)
├── tokens # Read-only: last response token count
├── new # Write anything to start fresh conversation
├── context # Read: conversation history; Write: add system message
├── _example # Read-only: usage examples
└── stream/ # Streaming interface
├── ask # Write-only: starts a streaming request
└── chunk # Read-only: blocks until next chunk, EOF on completion
| File | Read | Write |
|---|---|---|
ask |
Returns last LLM response | Sends prompt to LLM (sync), stores response |
model |
Returns current model name | Sets model for subsequent requests |
temperature |
Returns current temperature | Sets temperature (0.0-2.0) |
system |
Returns current system prompt | Sets system prompt (persists across resets) |
tokens |
Returns last response token count | Permission denied |
new |
Permission denied | Any write resets conversation (keeps system prompt) |
context |
Returns JSON conversation history | Appends system message to history |
_example |
Returns usage examples | Permission denied |
stream/ask |
Permission denied | Starts a streaming request |
stream/chunk |
Blocks until next chunk, returns it | Permission denied |
For long responses, use the streaming interface to see output as it's generated:
# Start a streaming request (using 9p tool)
echo "Write a poem about the moon" | 9p -a localhost:5640 write stream/ask &
# Read chunks as they arrive
while chunk=$(9p -a localhost:5640 read stream/chunk 2>/dev/null); do
[ -z "$chunk" ] && break
printf "%s" "$chunk"
doneWith a mounted filesystem:
# Start streaming in background
echo "Explain quantum computing" > /mnt/llm/stream/ask &
# Read chunks
while read -r chunk < /mnt/llm/stream/chunk 2>/dev/null; do
printf "%s" "$chunk"
doneNote: Start reading chunks immediately after writing to stream/ask. If you wait too long, the stream may complete and you'll get EOF.
#!/bin/sh
# ask.sh - Simple LLM query script
if [ -z "$1" ]; then
echo "Usage: $0 <question>"
exit 1
fi
echo "$1" > /mnt/llm/ask
cat /mnt/llm/ask| Flag | Default | Description |
|---|---|---|
-addr |
:5640 |
Address to listen on |
-backend |
api |
Backend: api (Anthropic API) or cli (Claude Code CLI) |
-debug |
false |
Enable debug logging |
| Variable | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
For api backend |
Your Anthropic API key |
- Model:
claude-sonnet-4-20250514(API) orsonnet(CLI) - Temperature:
0.7 - Max Tokens:
4096
| Feature | API Backend | CLI Backend |
|---|---|---|
| Authentication | API key required | Claude Max subscription |
| Token counting | Accurate | Not available (always 0) |
| Model names | Full names | Aliases (opus, sonnet, haiku) |
| Streaming | True streaming | Simulated (full response) |
| Rate limits | API limits apply | Subscription limits apply |
- Go 1.21+
- LLM backend (one of the following):
- Anthropic API key (for
apibackend) - Claude Code CLI with Max subscription (for
clibackend)
- Anthropic API key (for
- 9P client (one of the following):
# Check what's using the port
lsof -i :5640
# Use a different port
./llm9p -addr :5641Ensure the server is running and listening on the expected port:
# Start with debug logging to see connections
./llm9p -addr :5640 -debugInferno's DNS resolution works differently. Use the IP address directly:
# Wrong
mount -A tcp!localhost!5640 /n/llm
# Correct
mount -A tcp!127.0.0.1!5640 /n/llmCreate the mount point before mounting:
mkdir /n/llm
mount -A tcp!127.0.0.1!5640 /n/llmSome files are read-only by design:
tokens- Read-only (token count from last response)_example- Read-only (usage examples)stream/chunk- Read-only (streaming output)
The ask file only contains content after you write a prompt to it:
# First write a prompt
echo "Hello" > /mnt/llm/ask
# Then read the response
cat /mnt/llm/askCheck that your API key is set and valid:
# Ensure the key is exported
export ANTHROPIC_API_KEY=sk-ant-...
# Verify it's set
echo $ANTHROPIC_API_KEY- Server starts: llm9p listens for 9P connections on the specified port
- Client connects: A 9P client (9pfuse, Infernode, etc.) connects and negotiates the protocol
- Filesystem exposed: The client sees a virtual filesystem with files like
ask,model,tokens - Write prompt: Writing to
asksends the text to the configured LLM backend - Read response: Reading from
askreturns the LLM's response - State persists: Conversation history is maintained until you write to
new
The 9P protocol handles all the complexity of making this look like a regular filesystem, so any tool that can read and write files can interact with the LLM.
- Infernode - Hosted Inferno OS with native 9P support
- plan9port - Plan 9 from User Space
- u9fs - 9P file server
MIT