A complete walkthrough — install, see a decision, go to production. The compressed 60-second path lives in the README. This page covers everything else: troubleshooting, auth passthrough, the observation-mode workflow, Docker volume mounts, and building from source.
curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh | shThe script detects your OS and architecture, downloads the matching tarball from GitHub Releases, verifies its SHA256 against the published SHA256SUMS file, and installs the binary to /usr/local/bin (if writable) or $HOME/.local/bin. It also drops a default policy bundle and a starter steer.yaml under ~/.config/steer/. (At runtime, the binary additionally honors $XDG_CONFIG_HOME if set; the installer script itself writes to $HOME/.config/steer/.)
Inspect before running:
curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh -o install.sh
less install.sh
sh install.shPin a version:
STEER_VERSION=v0.1.0 curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh | shCustom install directory:
STEER_INSTALL_DIR=$HOME/bin curl -fsSL https://raw.githubusercontent.com/enforcegrid/steer/main/install.sh | shDirect download fallback (no curl ... | sh): pull the tarball + SHA256SUMS from the latest release, verify, extract.
# Foreground — decisions print to this terminal:
docker run --rm -p 8080:8080 ghcr.io/enforcegrid/steer
# Background + tail logs:
docker run -d --name steer -p 8080:8080 ghcr.io/enforcegrid/steer
docker logs -f steerTo mount your own policies and config:
docker run --rm -p 8080:8080 \
-v $(pwd)/steer.yaml:/app/steer.yaml:ro \
-v $(pwd)/policies:/app/policies:ro \
ghcr.io/enforcegrid/steerRequires Rust 1.86+:
git clone https://github.com/enforcegrid/steer.git
cd steer
cargo build --release
./target/release/steer --config steer.example.yaml --port 8080After install, steer resolves config in this order:
--config <path>if provided$XDG_CONFIG_HOME/steer/steer.yaml(default:~/.config/steer/steer.yaml)./steer.yaml(current working directory)
Start it with no flags — the install script bootstrapped a working config:
steerOr override the port:
steer --port 9090You should see something like:
INFO steer: loading config config=/Users/you/.config/steer/steer.yaml
INFO steer: steer starting version="0.1.0" addr=0.0.0.0:8080 fail_open=false
INFO steer: config wiring resolved policy_mode=enforce policy_dir=/Users/you/.config/steer/policies audit_backend=stdout audit_format=json
INFO steer: listening on 0.0.0.0:8080
If you see warnings like Cedar policy references PII pattern 'X' that is not compiled — that's the Cedar↔YAML consistency check. It means a policy enumerates a pattern name that isn't enabled in pii.patterns. Fix the yaml or remove it from the policy.
Point any coding agent or SDK at http://localhost:8080/v1 and send:
Steer terminal:
[BLOCK] POST /v1/chat/completions model=gpt-4o-mini block=default-exfiltration-request-block matched=markdown_img_data_url latency=0.7ms
The client receives an HTTP 403 — the request never reached the upstream LLM.
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"my key is sk-proj-abc123def456ghi789jklmnopqrstuvwx"}]}'[BLOCK] POST /v1/chat/completions model=gpt-4o-mini block=default-secrets-block matched=openai_key latency=1.1ms
[FLAG] POST /v1/chat/completions model=gpt-4o-mini flag=default-pii-flag matched=email latency=0.9ms
The request still reaches the upstream — flagging logs without blocking.
[ALLOW] POST /v1/chat/completions model=gpt-4o-mini latency=0.4ms
Your Authorization header passes through to the upstream LLM unchanged. Steer never reads, stores, or substitutes your API key — unless you opt in by setting providers.<name>.api_key in steer.yaml, in which case Steer overrides the inbound header with the configured key (useful for centralized key rotation in EE).
You can verify this against an empty Steer config:
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'Steer's pipeline scans the request body for policy violations, but the Authorization header is opaque to the policy engine. See docs/providers.md for the per-provider details.
Steer ships enforce-by-default so you see blocks fire on a fresh install. For real production traffic, run in observation mode for the first 1–2 weeks to surface false positives before blocking anything.
Edit ~/.config/steer/steer.yaml:
policy:
mode: observe # rewrites every @enforcement("block"|"steer") -> @enforcement("flag")Restart Steer. Every decision is still logged. Would-have-blocked events carry enforcement.observed: true. Filter them:
# All would-have-blocked events from the last hour:
jq 'select(.enforcement.observed == true and .timestamp >= (now - 3600 | todate))' audit.jsonl
# Group by rule_id to see which policies are loudest:
jq -r 'select(.enforcement.observed == true) | .enforcement.rule_id' audit.jsonl | sort | uniq -c | sort -rn
# Just the false-positive candidates with the matched payload:
jq 'select(.enforcement.observed == true) | {rule_id: .enforcement.rule_id, model: .request.model, patterns: [.labels[]?.metadata.pattern]}' audit.jsonlOnce the noisy stream is quiet for your traffic, flip back to mode: enforce and restart. Same binary, same policies, two postures.
For audit log persistence beyond stdout:
audit:
backend: file
log_path: /var/log/steer/audit.jsonl
format: jsonThe file backend is fail-loud: if /var/log/steer/ doesn't exist or isn't writable, Steer refuses to start. Silent fallback to stdout would compromise the audit trail.
| Symptom | Cause | Fix |
|---|---|---|
steer: command not found |
Install dir not on $PATH |
export PATH="$HOME/.local/bin:$PATH" and source ~/.zshrc (or ~/.bashrc) |
| Empty audit log file | Wrong path or permissions; tail -f started before traffic arrived |
Verify audit.log_path exists and is owned by the steer process; send one test request |
audit log file ... Refusing to start |
audit.backend: file with unwritable path |
Create the parent dir, fix ownership, or switch to backend: stdout |
| Port already in use | Another process on 8080 | steer --port 9090 or lsof -i :8080 to find the squatter |
| Policy not firing on a request | Pattern not in pii.patterns or detector didn't match |
Check startup logs for consistency warnings; run steer in foreground with audit.format: compact and send a test request — the compact line lists every detector that fired (matched=...) |
error: max retries exceeded from install.sh |
GitHub API rate limit (anonymous, 60/hr) | STEER_VERSION=v0.1.0 curl ... | sh skips the API call |
permission denied writing audit.jsonl in Docker |
Container UID 1000 vs host UID | Mount with :Z on SELinux, or --user $(id -u):$(id -g) |
| Decisions print but upstream returns 500 | Upstream provider down or rate-limited | Check audit.jsonl entries — response.status_code shows the upstream response |
Cedar evaluation failed 503s |
Policy syntax error after a hot-reload | Tail Steer's stderr; the offending file path is logged. Fix or remove the file. |
See docs/providers.md for per-tool walkthroughs:
- Cursor, Cline, Continue.dev — IDE plugin base URL override
- Claude Code (
ANTHROPIC_BASE_URL) — subscription-default alias gotcha - Aider —
--openai-api-baseflag - OpenAI / Anthropic SDKs —
base_urlparameter
- Authoring custom Cedar policies — your first policy in 10 minutes
- Architecture and audit-record schema — what the runtime does, what the audit emits
- Performance and capacity planning — benchmarks and methodology
- Compliance framework coverage — what evidence Steer produces, what it doesn't
- Enterprise features — SSO, cryptographically chained audit, support