@qitech/1pwd-proxy
v0.0.3
Published
Local HTTP proxy that resolves 1Password secret references (op://) via the 1Password CLI. Cross-platform autostart for Postman Community and similar tools.
Maintainers
Readme
@qitech/1pwd-proxy
Local HTTP proxy that pulls secrets from 1Password using your desktop biometric unlock. Drop-in solution for tools without native 1Password integration (Postman Community, curl, ad-hoc scripts, CI runners on a developer machine).
Cross-platform autostart: launchd (macOS), systemd user units (Linux), Task Scheduler (Windows).
5-minute setup
1. Install the 1Password CLI
| OS | command |
|---|---|
| macOS | brew install 1password-cli |
| Windows | winget install AgileBits.1Password.CLI |
| Linux | follow the official guide |
Then in the 1Password desktop app:
Settings → Developer → enable "Integrate with 1Password CLI"
Unlock the app once. From now on, op will use biometrics via the desktop.
Verify it works:
op vault listIf you see your vaults listed, you're good.
2. Install this proxy
npm install -g @qitech/1pwd-proxy3. Run the doctor to check everything
1pwd-proxy doctorYou'll see something like:
✓ op CLI installed (2.34.0)
✓ op CLI can talk to 1Password (desktop integration enabled)
✗ proxy not responding on port 7777 (not listening)
Run: 1pwd-proxy install (to register autostart)4. Start the proxy as a background service
1pwd-proxy installThis registers it to start automatically on login (launchd / systemd / Task Scheduler depending on your OS) and starts it now. Verify:
1pwd-proxy doctor
# all three checks should be ✓5. Pull a secret
curl "http://localhost:7777/op://YourVault/YourItem/password"You should see the secret value as plain text. Done — the proxy is ready to use.
6. (Optional) Generate a Postman pre-request script for your item
Instead of writing the script by hand and looking up each field id, let the CLI do it for you:
1pwd-proxy postman-snippet YourVault YourItemIt prints a ready-to-paste pre-request script with the right field ids and field names derived from your labels. Pipe to pbcopy (macOS) / clip (Windows) / xclip (Linux) to copy directly to your clipboard:
1pwd-proxy postman-snippet YourVault YourItem | pbcopySetting up 1Password for your secrets
If you already have your credentials in 1Password, skip ahead to Using with Postman.
Create a vault (optional but recommended)
A dedicated vault for automation credentials keeps things organized:
op vault create apisOr in the desktop app: sidebar → New vault.
Add an item
Pick a category in the desktop app that matches your use case:
| category | when to use | |---|---| | Login | Username + password (+ optional TOTP) for a service | | API Credential | API key with metadata (filename, valid range, etc.) | | Secure Note | Free-form: anything that doesn't fit elsewhere |
For credentials that don't fit the built-in fields (e.g. client_secret, tenant_id, api_key), click Add more while editing the item and add custom fields.
Find the field IDs of your item
The proxy resolves fields by id, not by display label. Built-in fields have stable ids (username, password, notesPlain). Custom fields and TOTP fields have generated ids.
List them with op:
op item get "MyItem" --vault "MyVault" --format json | jq '.fields[] | {id, label, type}'Example output:
{"id": "username", "label": "username", "type": "STRING"}
{"id": "password", "label": "password", "type": "CONCEALED"}
{"id": "notesPlain", "label": "notesPlain", "type": "STRING"}
{"id": "TOTP_abc123xyz", "label": "one-time password", "type": "OTP"}
{"id": "4a8b2c1d9e0f", "label": "client_secret", "type": "CONCEALED"}Notes on what you'll see:
| field type | id pattern | how to read it |
|---|---|---|
| Built-in (username, password, notesPlain) | the literal name | GET /op://Vault/Item/username |
| TOTP / one-time password | TOTP_<random> | GET /otp/Vault/Item (returns the live 6-digit code) |
| Custom fields | random alphanumeric | GET /op://Vault/Item/<id> |
The label is also usable as a reference (e.g. op://Vault/Item/client_secret), but the id is stable — if anyone renames the field's label, references-by-label break, references-by-id don't.
Endpoints
All requests are local (127.0.0.1 only).
| endpoint | what it does |
|---|---|
| GET /health | returns ok — use this to check the proxy is up |
| GET /op://<vault>/<item>/<field> | resolves a single field via op read (URL-encoded path) |
| GET /otp/<vault>/<item> | returns the current 6-digit OTP via op item get --otp. Use this when a TOTP field doesn't resolve via op read. |
| GET /item/<vault>/<item> | returns the full item JSON. One biometric prompt, all fields including the generated TOTP code in the totp property of the OTP field. Recommended when you need multiple fields from the same item. |
Examples
# health
curl http://localhost:7777/health
# single field
curl "http://localhost:7777/op://apis/MyService/password"
# whole item
curl http://localhost:7777/item/apis/MyService | jq
# TOTP (always 6 digits)
curl http://localhost:7777/otp/apis/MyServiceUsing with Postman
Postman Community has no built-in 1Password integration. With this proxy, a collection-level pre-request script can pull live secrets and inject them into requests without storing anything in environment variables.
Recommended pattern
In your collection's Scripts → Pre-request:
// clear any stale tokens lying around
pm.environment.unset('AccessToken');
pm.collectionVariables.unset('AccessToken');
// fetch the entire item in ONE biometric prompt
const item = await new Promise((resolve, reject) => {
pm.sendRequest(
'http://localhost:7777/item/apis/MyService',
(err, r) => {
if (err || r.code !== 200) return reject(err || r.text());
resolve(r.json());
}
);
});
// extract fields by id (use the ids you found via `op item get ... | jq`)
const fieldById = (id) => {
const f = item.fields.find(x => x.id === id);
if (!f) throw new Error(`field not found: ${id}`);
return f.totp || f.value;
};
const username = fieldById('username');
const password = fieldById('password');
const totp = fieldById('TOTP_abc123xyz'); // your TOTP field id
const client_secret = fieldById('4a8b2c1d9e0f'); // your custom field id
const client_id = pm.collectionVariables.get('client_id'); // non-secret static value
// example: OAuth password grant
const tokenRes = await new Promise((resolve, reject) => {
pm.sendRequest({
url: 'https://auth.example.com/oauth/token',
method: 'POST',
header: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: {
mode: 'urlencoded',
urlencoded: [
{key: 'grant_type', value: 'password'},
{key: 'client_id', value: client_id},
{key: 'client_secret', value: client_secret},
{key: 'username', value: username},
{key: 'password', value: password},
{key: 'totp', value: totp},
]
}
}, (err, res) => {
if (err) return reject(err);
resolve(res.json());
});
});
// expose to templates in LOCAL scope (cleared after this request finishes)
pm.variables.set('AccessToken', tokenRes.access_token);
pm.variables.set('OTP', totp);In each request's Headers tab:
| Key | Value |
|---|---|
| Authorization | Bearer {{AccessToken}} |
| OTP | {{OTP}} (only if your API requires it) |
pm.variables.set writes at local scope, which Postman discards after each request. The secrets never reach your collection/environment storage.
Simpler use case — single secret
If you only need one static field (no OAuth, no TOTP):
const password = await new Promise((resolve, reject) => {
pm.sendRequest(
'http://localhost:7777/' + encodeURIComponent('op://apis/MyService/password'),
(err, r) => r.code === 200 ? resolve(r.text()) : reject(err || r.text())
);
});
pm.variables.set('SERVICE_PASS', password);CLI reference
| command | what it does |
|---|---|
| 1pwd-proxy doctor | check op CLI, desktop integration, and proxy health |
| 1pwd-proxy start [--port N] | run in the foreground (use during development) |
| 1pwd-proxy install [--port N] | register autostart for your OS and start it now |
| 1pwd-proxy uninstall | remove autostart |
| 1pwd-proxy status | show service status |
| 1pwd-proxy logs | print log file paths |
| 1pwd-proxy postman-snippet <vault> <item> | generate a Postman pre-request script tailored to that item |
Default port is 7777, configurable via --port. The server always binds to 127.0.0.1 (no remote access).
How it works
Postman ──HTTP──> 1pwd-proxy (localhost:7777) ──exec──> op CLI ──IPC──> 1Password desktop ──biometric──> youThe proxy validates request paths with a strict regex and calls op via execFile (no shell interpolation). It does no caching — every request hits op, so values are always fresh. That matters for TOTP, where stale codes get rejected.
Troubleshooting
Run 1pwd-proxy doctor first — it tells you which of the three layers is broken.
| symptom | what to check |
|---|---|
| op CLI not found | Install with brew/winget/apt (see step 1 above) |
| op CLI cannot reach 1Password desktop | Open the desktop app → Settings → Developer → enable "Integrate with 1Password CLI", then unlock |
| proxy not responding | Run 1pwd-proxy install (or 1pwd-proxy start to test in foreground) |
| curl /op://... returns field not found | Verify the field id with op item get <item> --vault <vault> --format json \| jq '.fields[] \| {id, label}' |
| TOTP returns the seed instead of a 6-digit code | Use GET /otp/<vault>/<item> instead of GET /op://... |
Logs
| platform | location |
|---|---|
| macOS | ~/.1pwd-proxy/{out,err}.log |
| Linux | journalctl --user -u 1pwd-proxy.service |
| Windows | Event Viewer / Task Scheduler history |
Security notes
- The server binds to
127.0.0.1only — no remote access. - Reference paths are validated by regex before being passed to
execFile, blocking shell injection. oponly resolves secrets while the 1Password desktop is unlocked, so the attack surface is the same as any tool a logged-in user can run.- Resolved secret values are never logged — only timestamps, request paths, and error messages.
Uninstall
1pwd-proxy uninstall
npm uninstall -g @qitech/1pwd-proxyContributing / development
git clone <repo>
cd 1pwd-proxy
npm link # makes the local source executable as `1pwd-proxy`
1pwd-proxy doctorTest the server directly: node bin/cli.js start. Stop with Ctrl+C.
Releasing a new version
npm version patch # bumps version + creates git tag
npm publish # publishes under the @qitech scopeConsumers update with:
npm install -g @qitech/1pwd-proxy@latest
1pwd-proxy uninstall && 1pwd-proxy installThe uninstall/install cycle re-registers the autostart service against the new binary path.
