A zero-configuration developer CLI in Go that attaches to any active server process or container by port, PID, container name, or project name, and streams live stdout and stderr directly to your terminal.
# Launch interactive service picker
specter
# Attach directly by port, project, container, or PID
specter 3000
specter my-api-project
specter api-gateway-container
specter --pid 18492When running backend microservices, dev servers, or background containers across multiple terminal sessions, locating logs often requires hunting down hidden terminal tabs, querying container IDs, or restarting processes with verbose log wrappers.
Specter eliminates this workflow friction. Given a port, PID, container, or project nameβor simply running specter to open the interactive pickerβSpecter resolves the target, identifies where its standard streams are directed, attaches read-only (see Performance While Attached), and streams the output in real time with line reassembly, JSON formatting, and log-level coloring.
Specter runs on Linux, and on Windows through WSL2. Release files are Linux only: .tar.gz archives and .deb and .rpm packages for amd64 and arm64. There is no macOS or Windows download, because attaching to a process needs Linux kernel interfaces. On Windows, install it inside WSL2. On macOS, run your server and Specter in a Linux VM (for example OrbStack, UTM or Lima) or a Docker container.
1. One command (needs curl or wget):
curl -sSL https://raw.githubusercontent.com/YasserYG8/Specter/main/install.sh | shThe script checks that you are on Linux, downloads the release for your chip (amd64 or arm64), checks its SHA-256 against checksums.txt, and installs specter to /usr/local/bin (it uses sudo only if that folder is not writable). It installs nothing else: if strace or lsof is missing, it prints the command to install them. Set SPECTER_VERSION=v0.1.0 to pick a release, or SPECTER_INSTALL_DIR=$HOME/.local/bin to install without sudo.
2. Read the script first, if you do not like piping a script into sh:
curl -sSLO https://raw.githubusercontent.com/YasserYG8/Specter/main/install.sh
less install.sh
sh install.sh3. A package from the latest release page. The packages list strace, lsof and iproute2 as recommended packages:
sudo apt install ./specter_0.1.0_linux_amd64.deb # Debian, Ubuntu
sudo dnf install ./specter_0.1.0_linux_amd64.rpm # Fedora, RHEL4. With Go (1.26 or higher):
go install github.com/YasserYG8/Specter/cmd/specter@latestThis puts specter in $(go env GOPATH)/bin, so make sure that folder is on your PATH.
To uninstall what the script installed: sudo rm /usr/local/bin/specter.
If you did not use a package, set up the tools Specter uses:
sudo apt install strace lsof iproute2Specter has two ways to capture a terminal or a pipe. The ebpf engine reads the output inside the kernel and never stops the target, and it needs sudo and Linux 5.8 or later. The strace engine works everywhere, but it slows the target. Specter picks ebpf when it can run, and otherwise falls back to strace and tells you why in one line. Read Performance While Attached before pointing it at a busy server.
Ubuntu and WSL2 restrict ptrace by default, so attaching to a running process fails with Operation not permitted until you allow it. Either run sudo "$(which specter)" 3000, or see Kernel Permissions for the other options.
- Interactive Discovery Picker: Run
specterwith zero arguments to browse all active dev servers, listening ports, Docker containers, and project directories in a comfortable, keyboard-driven UI (β/βork/j,Enterto select). - Project-Aware Resolution: Automatically infers project names from your working directory, git repositories, or container labels (like the
portsCLI). Attach instantly viaspecter <project-name>. - Flexible Target Resolution: Attach by listening port (
specter 3000), process ID (specter --pid 14280), Docker container name or ID (specter web_app), or project name (specter example). - Multi-Process / Worker Tracing: Child worker processes (e.g. Node.js
cluster, Gunicorn, Celery, Puma, Unicorn) that already exist when you attach are always traced, and workers started later are followed.--treealso traces deeper descendants, up to 3 levels down. - Smart Log Pipeline: Reassembles split lines and multi-threaded streams, pretty-prints single-line JSON logs, highlights HTTP status codes, and groups multi-line stack traces (Node.js, Python, Go, Java).
- Zero Configuration & Strictly Read-Only: Works against already-running processes without code modifications, SDKs, or restarts. Never injects code or opens inbound network ports.
- Shell Autocompletion: Context-aware autocompletion for Bash, Zsh, Fish, and PowerShell that dynamically suggests active ports, container names, and projects.
Running specter without arguments scans the system and opens an interactive terminal interface:
[specter] Select an active dev server or container to stream (use β/β or j/k, Enter to select, Esc to quit):
TYPE PORT PROJECT PROCESS / IMAGE PATH / TARGET
ββββββ ββββββ βββββββββββββββ βββββββββββββββββββββββββ βββββββββββββ
β― host :3000 Specter specter-dev (PID 14280) /home/user/projects/Specter
docker :8080 billing-service node:20-alpine billing-api (c5e3f1a09d)
docker :5432 database postgres:16 postgres-db (a1b2c3d4e5)
host :6379 - redis-server (PID 2104) /etc/redis/redis.conf
In non-interactive environments (CI, pipes, scripts), Specter automatically outputs a cleanly aligned text table.
specter [port | pid | container | project] (or interactive picker)
β
βΌ
[Target Resolver]
βββ Port Resolver βββ Queries lsof / ss / procfs for listening TCP sockets
βββ Project Resolver βββ Inspects working directories, git root, package metadata
βββ Docker Resolver βββ Asks the `docker` CLI for mapped ports & containers
βββ Process Resolver βββ Reads /proc/<pid>/cmdline, cwd, fd targets
β
βΌ
[Worker Discovery] βββ Finds child worker PIDs; --tree also finds deeper descendants
β
βΌ
[Source Selector]
βββ Docker βββ Streams docker logs -f --tail 0
βββ Systemd Journal βββ Reads journalctl -f _PID=<pid>
βββ Regular File βββ Follows file descriptor target
βββ eBPF βββ Kernel programs read write/writev of the target; it is never stopped (root, Linux 5.8+)
βββ PTY / Syscall Trace βββ Attaches via strace (trace=write,writev on fds 1 & 2); slows the target
β
βΌ
[Line Assembler] βββ Thread/FD buffer assembly, flush timer, streaming UTF-8 decoding
β
βΌ
[Parser & Formatter] βββ Log-level detection, single-line JSON formatting, stack trace grouping
β
βΌ
[Output Renderer] βββ ANSI colored stdout stream (respects NO_COLOR and pipes)
- Resolution: Inspects network state using
lsoforssto identify the listening PID and checks the local Docker daemon for containerized port mappings. Inspects/proc/<pid>/cmdline,/proc/<pid>/cwd, and maps/proc/<pid>/fd/1and/proc/<pid>/fd/2. - Source Selection: Chooses the optimal acquisition mechanism:
- Docker Container (checked first): Streams through the
dockerCLI (docker logs). - Systemd Service: Streams via
journalctlif governed by systemd. - File Redirection: Follows file descriptors if stdout/stderr point to regular log files.
- eBPF (terminal or pipe, when this machine can run it: root or
CAP_BPFwithCAP_PERFMON, Linux 5.8 or later, and/sys/kernel/btf/vmlinux): small programs in the kernel keep only the target's ownwrite()andwritev()output, up to 64 KB per write, and hand it to Specter through a ring buffer. The target is never stopped and shows no tracer, so a process thatgdbor anotherstracealready traces can still be watched. Workers,--tree, writes through reopened terminal descriptors (Node.js) and the exit code work as withstrace. If the target writes faster than Specter reads, Specter says how many writes were lost. - Terminal / PTY, as the fallback: Uses
straceto watchwrite()andwritev()syscalls for descriptors 1 and 2, including writes from concurrent threads and worker processes. A child process is shown only when its output goes to the same place as the server's own stdout or stderr. Helpers that write to a private pipe or to/dev/null(for example the tools a Next.js dev server starts) are not shown, because their lines never reach your terminal either. Node.js servers started in a terminal (npm run dev, Next.js, Express, Vite) write through a descriptor that Node opened again from the terminal, such as fd 22, so Specter also follows any other descriptor that leads to the same terminal, pipe or socket as the server's stdout or stderr.
- Docker Container (checked first): Streams through the
- Stream Assembly: Hex buffers (
-xx) are decoded into raw byte chunks. TheLineAssemblerhandles thread interleaving, reassembles split lines, and flushes interactive prompts promptly. - Formatting & Filtering: Automatically categorizes log levels (
DEBUG,INFO,WARN,ERROR), highlights HTTP status codes, and groups multi-line stack traces before rendering.
# 1. Interactive picker (select from active ports, projects, containers)
specter
# 2. Attach by listening TCP port
specter 3000
specter 8080
# 3. Attach by project name (auto-detected from directory)
specter my-frontend-app
# 4. Attach by Docker container name or ID
specter redis-cache
specter 7a4b1c2d3e4f
# 5. Attach by Process ID (PID)
specter --pid 18492
specter -p 18492A bare number is always a port. If nothing listens on it, Specter says so and attaches to nothing. It never falls back to the process whose PID happens to be the same number, because that is almost never the server you meant. When such a process exists, the message names it and shows the command to attach to it:
[specter] No process is listening on port 3000.
...
Hint: PID 3000 exists (systemd-journald). To attach to it: specter --pid 3000
A number above 65535 cannot be a port, so specter 70000 attaches to PID 70000.
| Flag | Shorthand | Description |
|---|---|---|
--pid <pid> |
-p |
Attach to the process with this PID, whatever its value. It is never checked as a port. Give one PID, and not together with a port, container or project. |
--level <lvl> |
-l |
Minimum log level to display (debug, info, warn, error). An unknown value is an error. A line whose level Specter cannot find is always shown, never hidden. Levels are read from JSON and level= fields, [INFO], INFO:, ` |
--grep <pattern> |
-g |
Filter output by text or regular expression. A stack trace is shown whole when any of its lines matches, and not at all otherwise. To do this Specter holds a trace until it ends (up to 200 ms after its last line), so lines of other processes may appear before it. |
--stderr |
Stream only standard error (fd 2). Useful for isolating errors/crashes. Limit: when a Node.js server runs in a terminal, its output goes through reopened descriptors, and when stdout and stderr are the same terminal Specter cannot tell which one a line came from. Those lines count as stdout, so --stderr hides them. Redirecting the server's stderr separately (for example 2>err.log) makes the two distinct again. |
|
--tree |
-t |
Also trace deeper descendants of the target (up to 3 levels). Direct child workers are always traced. |
--source <name> |
-s |
Force a specific capture engine (docker, file, journal, ebpf, strace). --source ebpf never falls back: if eBPF cannot run, Specter says why and exits before attaching. --source strace keeps the engine that slows the target, and prints no tip. |
--duration <time> |
Detach after this time, for example 30s, 2m or 1h30m. The time starts when the capture starts. Specter prints Detached from PID N after 30s. and exits with status 0. A number without a unit (30) or a negative value is refused before anything is attached. Without the flag, or with 0, there is no limit. Works with every source. |
|
--json |
Output formatted single-line JSON records. | |
--raw |
Bypass line parsing and ANSI color formatting (pure raw stream). | |
--no-color |
Disable ANSI terminal color codes (also adheres to NO_COLOR). |
# Stream warnings and errors only from port 3000
specter 3000 --level warn
# Filter for authentication errors across worker processes (direct workers need no flag)
specter 8000 --grep "auth|unauthorized|invalid token"
# Isolate unhandled exceptions and crashes on stderr
specter 3000 --stderr
# Look for 30 seconds, then detach by itself (the target is slowed only for that long)
specter 3000 --duration 30s
# Trace a Docker container with raw JSON output
specter api-container --jsonSpecter prints its own status messages to stderr, so specter 3000 | grep 500 is never polluted by them. You may see:
[specter:notice] strace slows the target while Specter is attached. Use it on development and test servers, and use --duration to limit the time.Printed once, right after theSource:line, and only for thestraceengine. Whenstracewas chosen because the fast engine could not run, the same line ends with the reason, for exampleRun with sudo to use the fast eBPF engine.oreBPF needs Linux 5.8 or later (this kernel is 5.4).The other sources, includingebpf, do not slow the target.[specter:notice] 120 writes were lost because the target wrote faster than Specter could read.With theebpfengine only: the ring buffer between the kernel and Specter was full. Printed at most once per second, with the number of writes lost since the last notice, and never when nothing was lost. See Performance While Attached.[specter:notice] Target writes to its own log file <path>, which Specter does not follow.The process logs to a file instead of stdout/stderr. Specter names the file once, so you cantail -fit.[specter:notice] Nothing captured yet.Nothing was captured in the first 10 seconds. The target may be idle, buffering its output, or logging somewhere else. Output hidden by--levelor--grepdoes not trigger this.[specter:notice] --grep "..." is not a valid regular expressionThe pattern is matched as literal text instead.
A single write larger than 64 KB cannot be captured whole. Specter keeps the first 64 KB and marks the cut in the stream itself, on stdout, so you can see which line is damaged:
AAAA...AAAA[specter: 34465 more bytes not captured]
The marker is also shown with --raw. A line over 64 KB that was built from several smaller writes is cut when rendered and ends with ... [specter: truncated at 64KB].
If the server you are watching stops, Specter stops too, prints one line about how it ended, and exits with status 1:
[specter] Target PID 617 exited with code 3.
[specter] Target PID 595 was killed by SIGTERM.
[specter] Target PID 595 was killed by SIGKILL.
[specter] SIGKILL often comes from the kernel's out-of-memory killer. To check: dmesg | grep -i oom
[specter] Container billing-api exited with code 137.
When the exit code cannot be known (a process that writes to a log file, for example, because only a parent can read the exit code of a process after it is gone), the line is Target PID 595 exited. Specter notices within about a second.
Detached from PID N cleanly. is printed only when you stop Specter with Ctrl+C, and then the exit status is 0. After --duration the line is Detached from PID N after 30s., also with status 0. A script can tell the two apart by the exit status. A server that systemd restarts is not reported as stopped: when Specter follows a systemd unit, it keeps following it.
Specter has two engines for a terminal or a pipe, and they cost very different amounts.
ebpfreads the output inside the kernel and never stops the target. It needs root and Linux 5.8 or later, and Specter picks it when it can.stracestops the process at every system call, including the ones it does not print, so the cost depends on how often the server makes calls and writes. Specter uses it whenebpfcannot run, and says so in a notice.
Production use is not recommended yet. Use Specter on development and test servers. The strace engine should never be attached to a server you care about. The ebpf engine stayed under a 5% slowdown in the measurements below, but they come from two machines and one test server, and the engine is new.
These were measured with scripts/bench-overhead.sh: a small HTTP server that writes one log line per request, run as root. Each figure is the median of 5 runs of 10 seconds, with and without Specter attached. Latency is counted from the time each request was due, so a server that falls behind shows its queue.
Native Linux (GitHub runner, 4 cores of an AMD EPYC 7763, kernel 7.0, 2026-11-10). The runs varied by 1% to 6%.
| Engine | Load | Median latency without β with Specter | Requests per second without β with | Lines captured |
|---|---|---|---|---|
ebpf |
100 requests/s | 0.79 ms β 0.83 ms (+5%) | 100 β 100 | 100% |
ebpf |
1,000 requests/s | 0.65 ms β 0.68 ms (+4%) | 1,000 β 1,000 | 100% |
ebpf |
As fast as the server goes | 1.44 ms β 2.26 ms | 36,000 β 23,700 | 74% |
strace |
100 requests/s | 0.78 ms β 1.18 ms (+51%) | 100 β 100 | 100% |
strace |
1,000 requests/s | 0.67 ms β 1.01 ms (+52%) | 1,000 β 1,000 | 100% |
strace |
As fast as the server goes | 1.44 ms β 13.3 ms | 36,000 β 4,400 | 100% |
WSL2 (a 4-core laptop, AMD Ryzen 5 3500U, 2026-11-09). This machine is slower and much noisier: at 1,000 requests per second the ebpf runs with Specter ranged from 1.29 to 1.86 ms.
| Engine | Load | Median latency without β with Specter | Requests per second without β with | Lines captured |
|---|---|---|---|---|
ebpf |
100 requests/s | 2.25 ms β 2.35 ms (+4%) | 100 β 100 | 100% |
ebpf |
1,000 requests/s | 1.37 ms β 1.50 ms (+9.7%) | 1,000 β 1,000 | 100% |
ebpf |
As fast as the server goes | 2.6 ms β 4.0 ms | 16,000 β 10,200 | 79% |
strace |
100 requests/s | 2.1 ms β 5.4 ms | 100 β 100 | 100% |
strace |
1,000 requests/s | 1.3 ms β 819 ms | 1,000 β 885 | 100% |
strace |
As fast as the server goes | 3.9 ms β 243 ms | 10,400 β 225 | 100% |
What the numbers say:
- With
ebpf, the server keeps its rate at 100 and 1,000 requests per second, and the median latency rises by 4% to 5% on native Linux. Every line was captured. - With
strace, every call the server makes is stopped twice. On native Linux that added about 50% to the median latency at a fixed rate, and at full speed throughput fell by 88%. On the slower WSL2 laptop, at 1,000 requests per second the server could no longer answer on time, so requests queued and latency grew by hundreds of milliseconds. - At full speed, the
ebpfengine missed lines: it captured 74% on native Linux and 79% on WSL2. The server was not slowed by this. Specter's own processing of each line (parsing, formatting and printing) needed more than one core at that rate and could not keep up, so the buffer between the kernel and Specter filled and the kernel dropped writes. Specter counts the lost writes and printsN writes were lost because the target wrote faster than Specter could read., so a loss is never silent.stracecaptured every line there only because it slowed the server down to its own pace.
Your server will give different numbers. Run the script on a copy of it, or on a machine like it, before deciding: ENGINE=ebpf scripts/bench-overhead.sh or ENGINE=strace scripts/bench-overhead.sh. The full reports are in test/bench/results/. Two ways to limit the effect of strace: attach for a short time with --duration 30s, and use a source that does not slow the target when you can.
The slowdown ends when Specter detaches (Ctrl+C or the end of --duration). If Specter is killed, for example with kill -9, the kernel removes the eBPF programs, and makes strace detach by itself, so the server is not left traced or hooked. The docker, file and journal sources read output that already exists and do not slow the target. The Source: line Specter prints when it attaches tells you which one is in use.
Specter provides dynamic autocompletion for active ports, container names, and project directories.
# Bash
source <(specter completion bash)
# Zsh
source <(specter completion zsh)
# Or save to your zsh completion directory:
specter completion zsh > "${fpath[1]}/_specter"
# Fish
specter completion fish | source
# PowerShell (WSL2 / Windows)
specter completion powershell | Out-String | Invoke-ExpressionSpecter is engineered as a Linux-first systems tool leveraging Linux kernel interfaces (/proc, ptrace(2), lsof, ss, and the docker CLI).
- Linux: Native support on all standard distributions (Ubuntu, Debian, Fedora, Arch, Alpine).
- Windows: Fully supported via WSL2 (Windows Subsystem for Linux).
- macOS: Not supported natively. macOS System Integrity Protection (SIP) blocks
ptraceon other processes. Run your server and Specter inside a Linux VM (for example OrbStack, UTM or Lima) or a Docker container. Thelinux/arm64build covers VMs on Apple Silicon.
Running specter <target> on Windows or macOS stops with a message that says so and what to do. specter version, specter completion and --help work everywhere.
- Runtime:
lsof(oriproute2/ss),strace(for PTY/terminal snooping, and as the fallback when eBPF cannot run), and optionaldockerCLI. - For the
ebpfengine (optional, and faster): Linux 5.8 or later, kernel type information at/sys/kernel/btf/vmlinux, and root or the capabilitiesCAP_BPFandCAP_PERFMON. Most current Ubuntu, Debian and Fedora kernels have the type information. The compiled kernel program is built into thespecterbinary, so no compiler is needed. - Build: Go 1.26 or higher.
On Linux systems enforcing restricted ptrace access (/proc/sys/kernel/yama/ptrace_scope), attaching to non-child processes via strace may require elevated permissions:
# Check current ptrace scope (0 = unrestricted, 1 = restricted to child processes)
cat /proc/sys/kernel/yama/ptrace_scope
# Option A: Run Specter with sudo
sudo "$(which specter)" 3000
# Option B: Grant strace the ptrace capability permanently
sudo setcap cap_sys_ptrace+ep $(which strace)
# Option C: Temporarily allow ptrace snooping (resets on reboot)
sudo sysctl -w kernel.yama.ptrace_scope=0# Clone the repository
git clone https://github.com/YasserYG8/Specter.git
cd Specter
# Run tests
go test ./...
# Build binary
go build -o specter ./cmd/specter
# Cross-compile for Linux (from Windows PowerShell)
$env:GOOS="linux"; $env:GOARCH="amd64"; go build -o specter ./cmd/specterContributions are welcome. Native macOS and Windows attach is not possible with ptrace, so the open ideas for those systems are a Docker-only mode and a specter run mode that starts the server itself. See CONTRIBUTING.md to get started.