feat(synu): clean up agent env vars after exit

Amolith created

Agents can now declare which environment variables they set via a
_synu_agent_<name>_env_vars function. The main wrapper automatically
unsets these variables after the agent exits, preventing pollution of
the parent shell environment.

Changes:
- Added _synu_agent_claude_env_vars to declare ANTHROPIC_* and
  CLAUDE_CODE_* vars
- Added cleanup logic in synu.fish to call _env_vars and erase variables
- Fixed string match bug in claude.fish interactive mode (missing --
  separator)
- Updated AGENTS.md to document the new _env_vars function pattern

Assisted-by: Claude Sonnet 4.5 via Crush

Change summary

AGENTS.md                          | 366 ++++++++++++++++++++++++++++++++
functions/_synu_agents/claude.fish |  12 
functions/synu.fish                |   7 
3 files changed, 384 insertions(+), 1 deletion(-)

Detailed changes

AGENTS.md 🔗

@@ -0,0 +1,366 @@
+<!--
+SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+
+SPDX-License-Identifier: Unlicense
+-->
+
+# AGENTS.md - Developer Guide for synu
+
+This document helps AI agents work effectively with the synu codebase.
+
+## Project Overview
+
+**synu** is a universal wrapper for AI agents that tracks quota usage for [Synthetic](https://synthetic.new) API calls. It's written in Fish shell and provides:
+
+- Transparent quota tracking before/after agent execution
+- Agent-specific configuration system (e.g., model routing for Claude Code)
+- Interactive model selection with `gum`
+- Passthrough mode for agents without special configuration
+
+### Architecture
+
+```
+synu (main wrapper)
+  ├─ _synu_get_quota (API quota fetching)
+  └─ _synu_agents/ (agent-specific configuration)
+      └─ claude.fish (Claude Code model configuration)
+```
+
+**Control Flow:**
+1. Parse arguments (check for interactive mode, agent name)
+2. Load agent definition from `functions/_synu_agents/<agent>.fish` if it exists
+3. Parse agent-specific flags using `argparse` with `--ignore-unknown` for passthrough
+4. Configure agent environment (if agent has `_configure` function)
+5. Fetch initial quota from Synthetic API
+6. Execute agent with remaining arguments
+7. Fetch final quota and calculate/display session usage
+
+**Data Flow:**
+- Quota API → `_synu_get_quota` → space-separated "requests limit" string
+- Agent flags → `argparse` → `_flag_*` variables → agent `_configure` function
+- Interactive mode → `gum` + Synthetic API → model IDs → recursive `synu` call with flags
+
+## File Structure
+
+```
+├── functions/
+│   ├── synu.fish              # Main wrapper function
+│   ├── _synu_get_quota.fish   # Private: Fetch quota from API
+│   └── _synu_agents/
+│       └── claude.fish        # Claude Code agent configuration
+├── completions/
+│   └── synu.fish              # Shell completions
+├── crush.json                 # LSP configuration (fish-lsp)
+├── README.md                  # User documentation
+└── .gitignore                 # Ignore agent working directories
+```
+
+## Essential Commands
+
+This is a Fish shell library with no build system. Key commands:
+
+### Testing Manually
+
+```fish
+# Source the function (from repo root)
+source functions/synu.fish
+source functions/_synu_get_quota.fish
+
+# Check quota only
+synu
+
+# Test with an agent
+synu claude "What does this function do?"
+
+# Test interactive mode (requires gum)
+synu i claude "prompt"
+
+# Test with model override flags
+synu claude --opus hf:some/model "prompt"
+synu claude --large hf:some/model "prompt"
+```
+
+### Installation Testing
+
+```fish
+# Install via fundle (in a clean fish instance)
+fundle plugin 'synu' --url 'https://git.secluded.site/amolith/llm-projects'
+fundle install
+fundle init
+```
+
+### Checking for Syntax Errors
+
+```fish
+# Validate Fish syntax
+fish -n functions/synu.fish
+fish -n functions/_synu_get_quota.fish
+fish -n functions/_synu_agents/claude.fish
+fish -n completions/synu.fish
+```
+
+### LSP Integration
+
+The project includes `crush.json` configuring `fish-lsp` for Fish language support. The LSP should automatically provide diagnostics when editing `.fish` files.
+
+## Code Conventions
+
+### Fish Shell Style
+
+- **Indentation**: 4 spaces (no tabs)
+- **Variable naming**: `snake_case` with descriptive names
+  - Private/local vars: `set -l var_name`
+  - Global vars: `set -g var_name` (agent defaults)
+  - Exported vars: `set -gx VAR_NAME` (environment)
+- **Function naming**:
+  - Public: `synu`
+  - Private/helper: `_synu_*`
+  - Agent-specific: `_synu_agent_<name>_<action>`
+- **Comments**: Use `#` for explanatory comments focusing on *why* not *what*
+- **Error handling**: Check `$status` after critical operations, return non-zero on errors
+- **Error output**: Use `>&2` for all error messages
+- **SPDX headers**: All files must include SPDX copyright and license headers
+
+### Agent Configuration Pattern
+
+Each agent definition in `functions/_synu_agents/<agent>.fish` can provide four functions:
+
+1. **`_synu_agent_<name>_flags`** - Returns argparse flag specification (one per line)
+   ```fish
+   function _synu_agent_myagent_flags
+       echo "L/large="
+       echo "o/option="
+   end
+   ```
+
+2. **`_synu_agent_<name>_configure`** - Sets environment variables before execution
+   ```fish
+   function _synu_agent_myagent_configure
+       argparse 'L/large=' 'o/option=' -- $argv
+       or return 1
+       
+       # Configure based on flags
+       if set -q _flag_large
+           set -gx AGENT_MODEL $_flag_large
+       end
+   end
+   ```
+
+3. **`_synu_agent_<name>_env_vars`** - Returns list of environment variables to clean up after execution
+   ```fish
+   function _synu_agent_myagent_env_vars
+       echo AGENT_MODEL
+       echo AGENT_CONFIG
+   end
+   ```
+
+4. **`_synu_agent_<name>_interactive`** - Implements interactive model selection
+   ```fish
+   function _synu_agent_myagent_interactive
+       # Use gum to get user input
+       set -l selection (gum choose "option1" "option2")
+       or return 1
+       
+       # Return flags to pass to main synu call
+       echo --option=$selection
+   end
+   ```
+
+**Important patterns:**
+- Agents without definitions work as passthrough (no special config)
+- Use `argparse --ignore-unknown` in main wrapper so agent-native flags pass through
+- Interactive functions return flags that trigger recursive `synu` call
+- Configure functions receive flags already parsed from `_flag_*` variables
+- Environment variables listed in `_env_vars` are automatically unset after agent exits
+
+### API Integration
+
+**Synthetic API endpoints:**
+- Quota: `GET https://api.synthetic.new/v2/quotas`
+- Models: `GET https://api.synthetic.new/openai/v1/models`
+- Claude routing: `https://api.synthetic.new/anthropic`
+
+**Authentication**: Bearer token via `Authorization: Bearer $SYNTHETIC_API_KEY`
+
+**Response parsing**: Use `jq` with fallbacks (`// 0`) for missing fields
+
+**Error handling**: Check `curl` exit status and response emptiness before parsing
+
+## Important Gotchas
+
+### Argument Passing and argparse
+
+1. **Passthrough requires `--ignore-unknown`**: When parsing agent flags, use `argparse --ignore-unknown` so agent-native flags aren't consumed by synu's parser.
+
+2. **Flag variables after argparse**: After `argparse`, the `$argv` variable contains only non-flag arguments. Rebuild flag arguments from `_flag_*` variables to pass to configure function.
+
+3. **Preserve argument order**: When executing the agent with `command $agent $agent_args`, all original arguments (except synu-specific flags) must pass through unchanged.
+
+### Quota Tracking Edge Cases
+
+1. **API failures are non-fatal**: If quota fetch fails before/after execution, warn but continue. The agent should still run.
+
+2. **Default to zeros on failure**: If pre-execution quota fetch fails, use `"0 0"` to avoid math errors in post-execution display.
+
+3. **Exit status preservation**: Always return the agent's exit status (`$exit_status`), not the quota fetch status.
+
+### Environment Variable Scoping
+
+- Use `set -gx` for environment variables that need to be visible to child processes (the actual agent binary)
+- Agent defaults use `set -g` (global but not exported)
+- Local calculations use `set -l`
+
+### Interactive Mode Flow
+
+Interactive mode (`synu i <agent> [args...]`) works by:
+1. Calling agent's `_interactive` function to get flags
+2. Recursively calling `synu <agent> <flags> [args...]` (non-interactive)
+3. The recursive call then follows normal path with pre-parsed flags
+
+This means the `_configure` function must handle both direct flag input and flags from interactive mode identically.
+
+### Fish-specific Considerations
+
+- **Status checks**: Always use `test $status -ne 0` immediately after command, as any subsequent command overwrites `$status`
+- **Array slicing**: Use `$argv[2..-1]` syntax (1-indexed, inclusive)
+- **Command substitution**: Use `(command)` not `$(command)`
+- **String operations**: Use `string` builtin (`string split`, `string match`)
+- **No `local` keyword**: Use `set -l` for local scope
+
+## Testing Approach
+
+This project has no automated test suite. Testing is manual:
+
+1. **Syntax validation**: Run `fish -n` on all `.fish` files
+2. **Quota tracking**: Run `synu` alone, check quota display with color
+3. **Passthrough**: Test with unknown agent (`synu echo "hello"`)
+4. **Agent config**: Test Claude with/without flags
+5. **Interactive mode**: Test `synu i claude` with gum installed
+6. **Error cases**: Test without `SYNTHETIC_API_KEY`, with invalid API key
+
+**When making changes:**
+- Validate syntax with `fish -n`
+- Source the function and test manually with different argument combinations
+- Check that exit status is preserved
+- Verify quota display shows correct before/after values
+
+## Common Tasks
+
+### Adding a New Agent Configuration
+
+1. Create `functions/_synu_agents/<agent>.fish`
+2. Add SPDX header
+3. Define `_synu_agent_<agent>_flags` (return argparse flag specs)
+4. Define `_synu_agent_<agent>_configure` (set environment variables)
+5. Optionally define `_synu_agent_<agent>_interactive` (for `synu i <agent>`)
+6. Update `completions/synu.fish` to add the new agent to suggestions
+7. Test manually
+
+### Modifying Quota Display
+
+Quota display logic is in two places:
+- **No args case**: lines 6-28 of `synu.fish`
+- **Post-execution**: lines 128-166 of `synu.fish`
+
+Both use the same color thresholds:
+- Green: < 33% used
+- Yellow: 33-66% used
+- Red: > 66% used
+
+### Debugging API Issues
+
+```fish
+# Test quota fetch directly
+source functions/_synu_get_quota.fish
+set -gx SYNTHETIC_API_KEY your_key_here
+_synu_get_quota
+# Should output: "requests_used limit"
+
+# Test with curl directly
+curl -s -f -H "Authorization: Bearer $SYNTHETIC_API_KEY" \
+    "https://api.synthetic.new/v2/quotas" | jq .
+```
+
+### Understanding Model Configuration Flow (Claude)
+
+1. User runs: `synu claude --large hf:some/model "prompt"`
+2. `synu.fish` loads `functions/_synu_agents/claude.fish`
+3. Calls `_synu_agent_claude_flags` to get flag spec: `L/large=`, `o/opus=`, etc.
+4. Parses with `argparse --ignore-unknown` → sets `_flag_large`
+5. Rebuilds flags for configure: `--large=hf:some/model`
+6. Calls `_synu_agent_claude_configure --large=hf:some/model`
+7. Configure sets `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, etc.
+8. Executes `command claude "prompt"` with those environment variables
+9. Claude Code sees Synthetic models via env vars and uses them
+
+## Commit Conventions
+
+Use **Conventional Commits** with type and scope:
+
+**Types:** `feat`, `fix`, `refactor`, `docs`, `chore`, `test`
+
+**Scopes:**
+- `synu` - main wrapper function
+- `quota` - quota fetching/display
+- `agents/<name>` - specific agent configuration (e.g., `agents/claude`)
+- `completions` - shell completions
+- `docs` - documentation
+
+**Examples:**
+```
+feat(synu): add support for model override flags
+fix(quota): handle API errors gracefully
+feat(agents/claude): add interactive model selection
+docs(readme): document interactive mode
+```
+
+**Trailers:**
+- Use `References: HASH` for related commits/bugs
+- Always include `Assisted-by: [Model Name] via [Tool Name]` when AI-assisted
+
+## Project-Specific Context
+
+### Why Fish Shell?
+
+This is Amolith's personal project and Fish is their shell of choice. Fish provides:
+- Clean syntax without bashisms
+- Better error handling than POSIX shells
+- Native arrays and proper scoping
+- Built-in string manipulation
+
+### Why Synthetic API?
+
+Synthetic provides a unified API for multiple LLM providers with quota tracking. This wrapper makes quota visibility automatic and consistent across different agent tools.
+
+### Design Philosophy
+
+- **Minimal interference**: Pass through as much as possible to the underlying agent
+- **Graceful degradation**: If quota tracking fails, still run the agent
+- **Extensible**: Easy to add new agent configurations without modifying core wrapper
+- **Interactive when helpful**: Support both CLI flags and TUI selection
+
+### Known Agents
+
+Suggested in completions (may or may not have specific configurations):
+- `claude` - Claude Code (has configuration)
+- `crush` - Crush agent (passthrough)
+- `amp` - Amp agent (passthrough)
+- `octo` - Octo agent (passthrough)
+- `codex` - Codex agent (passthrough)
+
+Any command can be wrapped, these are just suggestions for completion.
+
+## Future Considerations
+
+Potential enhancements (not currently implemented):
+- Cache quota between rapid calls to reduce API hits
+- Support for other quota tracking APIs besides Synthetic
+- Agent-specific usage tracking/history
+- Budget warnings/limits
+- More agent configurations (crush, amp, etc.)
+
+When implementing features, maintain the core principles:
+- Don't break passthrough mode
+- Keep it simple and transparent
+- Graceful degradation
+- Follow Fish idioms

functions/_synu_agents/claude.fish 🔗

@@ -34,6 +34,16 @@ function _synu_agent_claude_flags --description "Return argparse-compatible flag
     echo "a/agent="
 end
 
+function _synu_agent_claude_env_vars --description "Return list of environment variables set by configure"
+    echo ANTHROPIC_BASE_URL
+    echo ANTHROPIC_AUTH_TOKEN
+    echo ANTHROPIC_DEFAULT_OPUS_MODEL
+    echo ANTHROPIC_DEFAULT_SONNET_MODEL
+    echo ANTHROPIC_DEFAULT_HAIKU_MODEL
+    echo CLAUDE_CODE_SUBAGENT_MODEL
+    echo CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
+end
+
 function _synu_agent_claude_configure --description "Configure Claude Code environment variables"
     # Parse flags passed from main synu
     argparse 'L/large=' 'l/light=' 'o/opus=' 's/sonnet=' 'H/haiku=' 'a/agent=' -- $argv
@@ -192,7 +202,7 @@ function _synu_agent_claude_interactive --description "Interactive model selecti
         if gum confirm "Save as default for 'claude'?"
             for flag in $flags
                 # Parse --key=value format
-                set -l parts (string match -r '^--([^=]+)=(.+)$' $flag)
+                set -l parts (string match -r -- '^--([^=]+)=(.+)$' $flag)
                 if test -n "$parts[2]"
                     set -l key $parts[2]
                     set -l value $parts[3]

functions/synu.fish 🔗

@@ -131,6 +131,13 @@ function synu --description "Universal agent wrapper with Synthetic API quota tr
     command $agent $extra_args $agent_args
     set -l exit_status $status
 
+    # Clean up environment variables set by agent
+    if functions -q _synu_agent_{$agent}_env_vars
+        for var in (_synu_agent_{$agent}_env_vars)
+            set -e $var
+        end
+    end
+
     # Fetch quota after agent execution
     set -l quota_after (_synu_get_quota)
     if test $status -ne 0