⚡ Sangsakala Proxy 🏠 Home
Skip to content
ag3ntsc4rnPublic

About

A free, local-first API client. Requests live as plain .http files in your git repo; secrets stay in your keychain.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

librepost

CI PyPI Python License: MIT

A free, local-first API client. Your requests are plain .http files in your git repo. Your secrets stay in your OS keychain. No account, no cloud, no telemetry, and nothing installed anywhere except the package itself.

uvx librepost-app ui      # or: uv tool install librepost-app && librepost ui

That opens a Postman-style UI in your browser, served from your own machine. (On PyPI the package is librepost-app; once installed, the command is librepost.)

Why

API clients have become cloud platforms: sign-ins, team workspaces, sync, and desktop apps that change your system without asking (see postman-app-support#14370). librepost goes the other way:

  • Requests are files. One folder, plain text, diffable in pull requests. The format is the .http format used by the JetBrains HTTP Client and the VS Code REST Client, so your collection also works in those tools.
  • Secrets are references. Write {{secret:keychain/github-token}}; the value is read from the keychain only when a request is sent, and is masked everywhere on screen.
  • Leaks are caught. librepost check (also a pre-commit hook) flags tokens and literal credentials before they reach git.
  • It stays in its lane. librepost reads and writes .http files in the folder you point it at, and nothing else. No auto-updates, no plugins, no background services.

Install

The package is librepost-app on PyPI; the command it installs is librepost.

Method Install Upgrade
uv (recommended) uv tool install librepost-app uv tool upgrade librepost-app
uvx (no install) uvx librepost-app ui always runs the latest release
pipx pipx install librepost-app pipx upgrade librepost-app
pip pip install librepost-app pip install -U librepost-app

On macOS with Homebrew Python and on recent Linux distributions, a global pip install stops with an "externally-managed-environment" error. Use uv or pipx, or install into a virtual environment:

python3 -m venv ~/.venvs/librepost
~/.venvs/librepost/bin/pip install librepost-app
~/.venvs/librepost/bin/librepost ui

On Windows, if librepost isn't found after py -m pip install librepost-app, Python's Scripts folder isn't on your PATH; py -m librepost ui works either way.

librepost never updates itself. You upgrade when you choose to.

Desktop window (optional)

To use librepost in its own window instead of a browser tab, install the desktop extra and add --window:

uv tool install --force "librepost-app[desktop]"   # or: pip install "librepost-app[desktop]"
librepost ui --window

It uses the web view built into your system (pywebview): WebKit on macOS and Edge WebView2 on Windows (included in Windows 10 and 11). On Linux pywebview also needs GTK or Qt; the easiest is Qt: uv tool install --force "librepost-app[desktop]" --with "pywebview[qt]".

It's the same local UI with the same protections, plus two more: browser extensions can't see the page, and nothing (cookies, storage) is kept after the window closes. Closing the window stops librepost.

Quick start

librepost init my-api        # example env file + requests; gitignores the private env file
cd my-api
librepost ui                 # opens http://127.0.0.1:8765/?token=...

Or from the terminal:

librepost run requests/example.http --name post-example --env dev -v

Import from Postman, a browser or curl

librepost import my-api.postman_collection.json      # Postman collection (v2.0/v2.1)
librepost import staging.postman_environment.json    # Postman environment or globals
librepost import session.har                         # browser devtools: "Save all as HAR"
librepost import "curl -X POST https://api.example.com/items -d name=x"
pbpaste | librepost import -                         # "Copy as cURL" from your browser

In the UI, click Import… to paste a curl command or JSON, or to upload an export.

Credentials never land in files. Literal tokens, passwords, API keys and cookies are replaced with {{secret:keychain/...}} references. Add --save-secrets to put the values found in the import straight into your keychain; otherwise librepost prints the librepost secret set commands to run. Postman exports often leave secret values blank, and placeholders like "Your API key" are not treated as real values.

From What you get
Postman collection one .http file per folder, folders as directories; collection variables in $shared; collection and folder auth inherited per request
Postman environment an environment in http-client.env.json
HAR one file per host; only fetch/XHR and page requests (--all keeps everything); browser-only headers and repeats dropped
curl requests appended to imported.http (--file to change); handles bash $'...' quoting from browsers

Imports never overwrite: a clashing file gets a numbered name, and existing environment values are kept. Use --dry-run to preview. Common Postman test-script statements (setting a variable from the response, checking the status, a header, the response time or a JSON value) become @capture and @expect lines; collection and folder scripts apply to every request inside them. Not imported yet: pre-request scripts, other script code, file uploads, OAuth 1.0 and other signed auth schemes; each is reported as a warning.

Requests

@apiVersion = v2

# @name create-user
POST {{baseUrl}}/{{apiVersion}}/users
Content-Type: application/json
Authorization: Bearer {{token}}

{"name": "Ada", "requestId": "{{$uuid}}"}

###

# @name list-users
GET {{baseUrl}}/{{apiVersion}}/users?page=1
  • Requests are separated by ###; # @name gives one a name.
  • Authorization: Basic user password is encoded for you, so the password can be a {{secret:...}} reference.
  • @name = value lines define file variables.
  • Built-in values: {{$uuid}}, {{$timestamp}}, {{$isoTimestamp}}, {{$randomInt}}.
  • Response handler scripts (> {% ... %}) are kept intact when you save, but are not run.

Checks and chaining

Test responses and pass values between requests, without scripts. Write the lines above the request (or in the editor's Checks tab); other .http tools treat them as comments.

# @capture token = $.access_token
# @capture userId = $.user.id
# @expect status == 200
# @expect $.user.roles contains admin
# @name login
POST {{baseUrl}}/login
Content-Type: application/json

{"user": "ada", "password": "{{secret:keychain/ada-password}}"}

###

# @expect status == 200
# @expect duration < 500
# @name profile
GET {{baseUrl}}/users/{{userId}}
Authorization: Bearer {{token}}
  • @capture name = … keeps a value for later requests as {{name}}, until you clear it or stop librepost. @capture-secret masks it everywhere, and so does any capture named like a credential (token, password, apiKey, …).
  • @expect <subject> <operator> <value> shows ✓ or ✗ under the response; librepost run prints them and exits with status 1 if one fails.
  • Subjects: status, duration (ms), body, header Name, JSON paths like $.items[0].id or $.items.length, and regex "pattern" (first group) for HTML or text.
  • Operators: == != < <= > >= contains !contains matches exists !exists. Values are JSON (200, "text", true, null) or a bare word.

Run in CI

librepost test runs every request in order (files sorted by path, requests in file order), carrying captured values along, and exits with status 1 if a check fails or a request can't be sent. It replaces Newman for collections kept as .http files.

librepost test requests/ --env ci --junit report.xml
librepost test requests/auth.http --bail      # stop at the first failure
# .github/workflows/api-tests.yml
jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v6
      - run: uvx librepost-app test requests/ --env ci --junit report.xml
        env:
          API_TOKEN: ${{ secrets.API_TOKEN }}   # referenced as {{secret:env/API_TOKEN}}

Secrets and captured credentials are masked in the output and never written to the report. Use {{secret:env/NAME}} references in CI, where there's no keychain.

Client certificates, CAs and proxies

Set them per environment on the Environments screen (Connection), or in http-client.private.env.json using the JetBrains HTTP Client format:

{
  "dev": {
    "SSLConfiguration": {
      "clientCertificate": "~/.certs/me.pem",
      "clientCertificateKey": "~/.certs/me-key.pem",
      "certificatePassphrase": "{{secret:keychain/dev-cert-passphrase}}",
      "caCertificate": "~/.certs/corp-ca.pem",
      "verifyHostCertificate": true
    },
    "ProxyConfiguration": {"url": "http://me:{{secret:keychain/proxy}}@proxy.corp:8080"}
  }
}
  • Private file only. These settings are ignored (with a warning) in the committed http-client.env.json, so a repository you clone can't route your requests through its proxy or switch off certificate checks.
  • Client certificate and key as PEM files (or both in one file); an encrypted key needs certificatePassphrase, ideally a keychain reference. Convert .p12/.pfx with openssl pkcs12 -in cert.p12 -out cert.pem -nodes.
  • caCertificate is added to the usual trusted CAs, for internal services.
  • $shared settings apply to every environment. HTTPS_PROXY, NO_PROXY and SSL_CERT_FILE from your shell also work.
  • Responses over HTTPS show the TLS version and the server certificate's subject and issuer (librepost run -v prints the cipher and expiry too), or a warning when it wasn't verified.

Environments

Same format as the JetBrains HTTP Client:

File Commit it? Holds
http-client.env.json yes shared, non-secret values per environment
http-client.private.env.json no (gitignored by librepost init) per-machine overrides
{
  "$shared": {"apiVersion": "v2"},
  "dev":  {"baseUrl": "https://dev.example.com", "token": "{{secret:keychain/dev-token}}"},
  "prod": {"baseUrl": "https://api.example.com", "token": "{{secret:keychain/prod-token}}"}
}

Secrets

Reference Value comes from
{{secret:keychain/<name>}} the OS keychain: macOS Keychain, Windows Credential Manager, or Secret Service (GNOME Keyring, KWallet) on Linux
{{secret:env/<NAME>}} an environment variable, handy in CI and on headless machines
librepost secret set dev-token      # prompts without echoing
librepost secret list               # names only, never values
librepost secret delete dev-token

In the UI, Secrets lists every reference in your workspace with its status and lets you set or replace values (never read them), and Environments edits both env files, with a 🔒 box that moves a value into the keychain.

Secret values are masked (••••••) in the request preview, response headers and response body (APIs often echo credentials back), and in error messages.

History and git

History lists the requests you've sent from the UI, newest first, with status, timing and check results. Open one to see its response again, or Open in editor to get a copy of the request as written (placeholders included) and send it again. The History button on a saved request shows only its runs.

History is kept on your computer only and outside your workspace, so it can't be committed: ~/Library/Application Support/librepost/history on macOS, %LOCALAPPDATA%\librepost\history on Windows, ~/.local/state/librepost/history elsewhere. Secret values are masked before anything is written, files are readable by you only, and the newest 200 requests per workspace are kept. Turn it off with librepost ui --no-history, or clear it from the History screen.

If the workspace is in a git repository, the top bar shows the branch, how many files changed, and how far you are ahead of or behind the upstream. Changed request files get a letter in the sidebar (M modified, U untracked, A added, D deleted, R renamed), and Changes lists them with a diff against the last commit. librepost only reads from git: commit with your usual tools.

Leak check

librepost check              # exit code 1 if anything looks like a secret

It looks for known token formats (GitHub, AWS, Slack, Stripe, Google, OpenAI, Anthropic, JWTs, private keys), literal values in Authorization, Cookie and API-key headers, literal passwords and tokens in http-client.env.json, and a private env file that isn't gitignored.

As a pre-commit hook:

- repo: https://github.com/ag3ntsc4rn/librepost
  rev: v0.8.0
  hooks:
    - id: librepost-check

How the local UI is protected

A local web server that sends HTTP requests for you would be an open door into your network for any website you visit, if it weren't locked down. librepost:

  • listens on 127.0.0.1 only, never on your network;
  • requires the random token printed at startup; the token is swapped for an HttpOnly, SameSite=Strict cookie scoped to that port;
  • rejects any Host header other than 127.0.0.1:<port>/localhost:<port>, which blocks DNS rebinding;
  • rejects state-changing requests without a same-origin Origin header and htmx's request header, so other sites can't make it send requests;
  • sends a strict Content-Security-Policy (no inline or third-party scripts), shows response bodies only as escaped text, and marks every response Cache-Control: no-store.

Found a hole? Please report it privately; see SECURITY.md.

Requirements

Python 3.11+ (uv fetches one for you if needed) and a web browser. Git is optional.

Roadmap

  • Commit from the UI, and comparing two responses from history
  • More secret providers: 1Password CLI, HashiCorp Vault, pass

Contributing

Contributions are welcome. See CONTRIBUTING.md.

License

MIT. The bundled htmx is under the Zero-Clause BSD licence.

About

A free, local-first API client. Requests live as plain .http files in your git repo; secrets stay in your keychain.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages