@kopya/cli
v0.2.0
Published
Record your app's real API calls once, replay them offline forever.
Downloads
31
Maintainers
Readme
kopya
Record your app's real API calls once. Replay them offline, forever.
kopya (Filipino for "copy") is a single static binary that sits between your app and
the internet. Run your test suite once against the real API, and every call after that is
served from a fixture file — no network, no API bill, no flakes.
$ kopya record -- pytest tests/
...
kopya recorded 47 calls → .kopya/fixtures.json
Secrets were redacted. Review the file before committing it.
$ kopya replay -- pytest tests/
kopya loaded 47 recorded calls from .kopya/fixtures.json
...
kopya replayed 47 calls · 0 live
Saved ~128,400 tokens and 94.2s.Your application code does not change. No mock library, no dependency injection, no monkeypatching.
Why
Tests that hit OpenAI, Anthropic, Stripe or any other third-party API are slow, nondeterministic, and cost real money. The existing answers all have a catch: language -bound mock libraries only cover one runtime, and the proxy-based tools need Python or a JVM in your CI image.
kopya is one ~7MB binary with no runtime. It boots instantly in a container and works the
same for Python, Node, Go, Rust, Ruby, Java or curl.
Install
Whichever ecosystem you already have. All three install the same prebuilt binary;
nothing is compiled or downloaded at install time. (The npm package is a small
Node shim that execs the binary, so npx invocations pay Node's startup once.)
npx @kopya/cli --help # Node
uvx kopya --help # Python
cargo install kopya # Rust(The npm package is scoped @kopya/cli; the installed command is still kopya.)
Or grab a binary from the releases page. Prebuilt for macOS (Intel and Apple Silicon), Linux (x64 and arm64, statically linked against musl) and Windows x64.
How it works
kopya runs an HTTP/HTTPS proxy on 127.0.0.1 and launches your command with the proxy and
CA environment variables already set:
| Variable | Why |
| --- | --- |
| HTTP_PROXY / HTTPS_PROXY | Routes traffic through kopya |
| NODE_USE_ENV_PROXY=1 | Node's built-in fetch ignores HTTP_PROXY without it |
| NODE_EXTRA_CA_CERTS | Node uses its own CA bundle, not the system keychain |
| REQUESTS_CA_BUNDLE | Python requests / httpx use certifi |
| SSL_CERT_FILE, CURL_CA_BUNDLE | OpenSSL and curl |
This matters: trusting a root CA in the system keychain is not enough for Node, Python
or Java. Setting these per-process is what makes "zero code changes" actually true. If you
point an already-running process at kopya instead, run kopya ca trust and export the
variables it prints.
Commands
kopya record -- <command> # run <command>, save its outbound calls
kopya replay -- <command> # run <command> against the saved calls
kopya ca path # print the root CA path (creates it on first run)
kopya ca trust # install the root CA into the system trust store
kopya ca untrust # remove itUseful flags:
--out/--in— fixture file path (default.kopya/fixtures.json)--port— proxy port (default8888;0picks a free one)--append— keep calls already in the fixture that this run did not re-record--no-proxy— hosts to reach directly (defaultlocalhost,127.0.0.1,::1)--allow-live— let unmatched requests reach the network instead of failing--realtime— reproduce the original timing of recorded streams--no-redact— keep credentials in the fixture. Don't.
Loopback is excluded by default, so a local test server, a testcontainer or a
WebDriver on 127.0.0.1 is not swept into the fixture. Pass --no-proxy "" to
record those too.
Running kopya record or kopya replay with no -- command starts the proxy in the
foreground so you can point an existing process at it.
Streaming responses
text/event-stream responses — every streaming LLM API — are recorded frame by
frame, with the offset each frame arrived at, and replayed with those boundaries
intact. A recording is passed through to your app live as it happens, so the run
you record behaves exactly like an unproxied one.
By default replay emits the frames as fast as the client will take them, because the point of replay is to make a slow test fast:
recorded 7 frames over 1015ms
replayed 7 frames over 7ms # 145x faster, same frames
replayed 7 frames over 1012ms # with --realtimeUse --realtime when the thing under test is the streaming behaviour itself —
time-to-first-token, incremental rendering, or a client-side timeout.
Token usage is parsed out of the stream — OpenAI's usage.total_tokens,
Anthropic's split input_tokens/output_tokens, and Gemini's
usageMetadata — so the summary can report what a replayed run would have cost.
A stream that never reached its end, because the client hung up or the command
exited mid-flight, is recorded with "truncated": true and called out in the
summary. It will replay short, so re-record it if a test depends on the end.
Only text/event-stream is streamed. Other chunked responses — NDJSON, long
polls, large downloads — are buffered whole before being passed on.
Strict by default
In replay, a request with no recorded match returns 502 and fails the run, even if
your test swallowed the error. A green replay run therefore proves the suite is genuinely
hermetic. Pass --allow-live when you want the softer behaviour.
Fixtures are meant to be committed
The fixture file is the artifact your team shares, so it is designed to survive code review:
- Headers are stored in sorted maps and bodies as plain text, so diffs stay readable
- Credentials are redacted before anything touches disk —
Authorization,Cookie,x-api-keyand friends, plus baresk-/ghp_/AKIA-style tokens found in bodies, form fields, and query strings like Google's?key= - Binary payloads are dropped rather than base64-inflating the file
Redaction only ever touches the stored copy. Match keys are always derived from the request as it arrived on the wire, so redacting a request body cannot stop it matching on replay.
Repeat calls to the same endpoint are kept in order and replayed in order, so polling and pagination work. Once a sequence is used up its last response repeats.
Always read a fixture before committing it the first time. Redaction is conservative by design, not a guarantee.
Status
Early. 0.2 covers HTTP/1.1 record and replay, including server-sent event streams,
with redaction, response decompression and strict matching.
Fixtures are format version 2. Version 1 files do not load — kopya will say so and
ask you to re-record.
Roadmap: HTTP/2, a GitHub Action, Homebrew, request-count assertions, and latency + error injection.
Not planned for 0.1: gRPC, WebSocket, a GUI, declarative YAML rule files.
License
Apache License 2.0 — © 2026 ThinkGrid Labs
