Last active
September 15, 2026 13:20
-
-
Save asaf400/2aba64b822e8cbcc0a1f38541ee7ff10 to your computer and use it in GitHub Desktop.
1Password CLI wrapper for WSL and 1password integration for runtime secrets injection. Made by LLM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """Drop-in replacement for `op run` on WSL, where `op run` cannot exec Linux binaries. | |
| `op.exe run -- cmd` spawns cmd as a *Windows* process, so it can never launch a | |
| Linux program. This resolves every secret with a single `op.exe inject` call over | |
| the interop pipe, then execs the target Linux process with the resolved | |
| environment in place. | |
| Usage: | |
| op-run [--env-file FILE]... [VAR=op://vault/item/field]... -- cmd [args...] | |
| op-run [--env-file FILE]... --print # emit shell-quoted exports | |
| Env files accept `VAR=op://...` and `VAR={{ op://... }}`; `#` comments and blank | |
| lines are ignored. Values that are not secret references are passed through | |
| verbatim. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import secrets | |
| import shlex | |
| import shutil | |
| import subprocess | |
| import sys | |
| # Random per-run sentinel so a secret's own bytes can never be mistaken for a | |
| # field delimiter. Secrets may legitimately contain newlines, quotes and '=' | |
| # (PEM keys, JSON blobs), which is why line-oriented parsing is not enough. | |
| SENTINEL = f"__OPRUN_{secrets.token_hex(16)}__" | |
| REF_RE = re.compile(r"^\{\{\s*(op://.*?)\s*\}\}$|^(op://.*)$", re.DOTALL) | |
| NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") | |
| def die(msg: str) -> "NoReturn": # type: ignore[valid-type] | |
| print(f"op-run: {msg}", file=sys.stderr) | |
| raise SystemExit(2) | |
| def find_op() -> str: | |
| override = os.environ.get("OP_EXE") | |
| if override: | |
| return override | |
| for candidate in ("op.exe", "/mnt/c/ProgramData/chocolatey/bin/op.exe"): | |
| found = shutil.which(candidate) or (candidate if os.path.exists(candidate) else None) | |
| if found: | |
| return found | |
| die("could not find op.exe on PATH; set OP_EXE to its full path") | |
| def parse_env_file(path: str) -> list[tuple[str, str]]: | |
| pairs: list[tuple[str, str]] = [] | |
| with open(path, encoding="utf-8") as handle: | |
| for lineno, raw in enumerate(handle, 1): | |
| line = raw.strip() | |
| if not line or line.startswith("#"): | |
| continue | |
| if line.startswith("export "): | |
| line = line[len("export ") :].lstrip() | |
| if "=" not in line: | |
| die(f"{path}:{lineno}: expected VAR=value, got {raw.rstrip()!r}") | |
| name, value = line.split("=", 1) | |
| pairs.append((name.strip(), _unquote(value.strip()))) | |
| return pairs | |
| def _unquote(value: str) -> str: | |
| if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"": | |
| return value[1:-1] | |
| return value | |
| def resolve(pairs: list[tuple[str, str]], op_exe: str) -> dict[str, str]: | |
| """Resolve every op:// reference in one op.exe inject call. | |
| Later assignments win, so `--env-file` secrets can be overridden by a | |
| subsequent plain `VAR=value` (or the other way around). | |
| """ | |
| last: dict[str, str] = {} | |
| for name, value in pairs: | |
| if not NAME_RE.match(name): | |
| die(f"invalid variable name {name!r}") | |
| last[name] = value | |
| resolved: dict[str, str] = {} | |
| template_lines: list[str] = [] | |
| deferred: list[str] = [] | |
| for name, value in last.items(): | |
| match = REF_RE.match(value) | |
| if match: | |
| ref = match.group(1) or match.group(2) | |
| template_lines.append(f"{name}={SENTINEL}{{{{ {ref} }}}}{SENTINEL}") | |
| deferred.append(name) | |
| else: | |
| resolved[name] = value | |
| if not deferred: | |
| return resolved | |
| template = "\n".join(template_lines) + "\n" | |
| proc = subprocess.run( | |
| [op_exe, "inject"], | |
| input=template.encode(), | |
| stdout=subprocess.PIPE, | |
| stderr=None, | |
| check=False, | |
| ) | |
| if proc.returncode != 0: | |
| raise SystemExit(proc.returncode) | |
| out = proc.stdout.decode("utf-8", errors="surrogateescape") | |
| found = dict( | |
| re.findall( | |
| rf"^([A-Za-z_][A-Za-z0-9_]*)={SENTINEL}(.*?){SENTINEL}$", | |
| out, | |
| re.DOTALL | re.MULTILINE, | |
| ) | |
| ) | |
| missing = [name for name in deferred if name not in found] | |
| if missing: | |
| die(f"op.exe inject did not return values for: {', '.join(missing)}") | |
| resolved.update(found) | |
| return resolved | |
| def main(argv: list[str]) -> int: | |
| pairs: list[tuple[str, str]] = [] | |
| print_mode = False | |
| command: list[str] = [] | |
| index = 0 | |
| while index < len(argv): | |
| arg = argv[index] | |
| if arg == "--": | |
| command = argv[index + 1 :] | |
| break | |
| if arg == "--print": | |
| print_mode = True | |
| elif arg == "--env-file": | |
| index += 1 | |
| if index >= len(argv): | |
| die("--env-file requires a path") | |
| pairs += parse_env_file(argv[index]) | |
| elif arg.startswith("--env-file="): | |
| pairs += parse_env_file(arg.split("=", 1)[1]) | |
| elif "=" in arg and not arg.startswith("-"): | |
| name, value = arg.split("=", 1) | |
| pairs.append((name, value)) | |
| else: | |
| die(f"unrecognised argument {arg!r}") | |
| index += 1 | |
| if not pairs: | |
| die("no secrets requested") | |
| if not print_mode and not command: | |
| die("no command given; use `-- cmd args` or --print") | |
| resolved = resolve(pairs, find_op()) | |
| if print_mode: | |
| for name, value in resolved.items(): | |
| print(f"export {name}={shlex.quote(value)}") | |
| return 0 | |
| env = {**os.environ, **resolved} | |
| try: | |
| os.execvpe(command[0], command, env) | |
| except FileNotFoundError: | |
| die(f"command not found: {command[0]}") | |
| if __name__ == "__main__": | |
| raise SystemExit(main(sys.argv[1:])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment