You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Lead with the unquoted command form, and take -- the way every tool does (#39)
keycmd already ran a command written out as separate arguments, but the
docs treated it as the exception: every example put the command in quotes,
which reads as a requirement rather than as the tool for the one job it is
needed for. Quotes are what you reach for when you want a shell — a pipe,
an &&, a credential expanded into the command line — and almost nothing
else needs one. `keycmd npm install` is the form the docs now lead with,
on the landing pages, in the quick start, in the examples and in the guide,
which is also what the README meant by "prefix any command with keycmd".
Writing a command out that way brings `--` with it, since every tool that
goes on to run another one takes it. keycmd took it as the first word of
the command instead, so `keycmd -- npm install` reached the shell as
`-- npm install` and failed on it. `end_of_options` removes the one that
ends keycmd's own options, which also makes a command whose first word
starts with a dash reachable; any further `--` is the command's own and is
passed along untouched.
--help gains a usage line and examples covering both forms, since that is
where someone looks before the docs.
The one place the unquoted form cannot keep its word boundaries is WSL,
where wsl.exe strips the quotes off its own command line before the
distribution sees them. That is the platform's limit rather than keycmd's,
and the WSL guide now says so.
Claude-Session: https://claude.ai/code/session_01HFqRvGXvoDW1tiX7dJHoYb
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -44,7 +44,7 @@ The README is a landing page and nothing more: what it says about behaviour it s
44
44
45
45
## Architecture
46
46
47
-
`cli.main` wires the three halves together: `load_conf` produces the configuration, `get_env` turns it into an environment, and `run_cmd`/`run_shell` hand that environment to a shell. `--detect-backend` and `--reset-backend` return before any of it, since neither has a use for a configuration or a command. Errors reach the user through `logs.error`, which exits with status 1 and takes the hint lines that go under the error with it; `logs.vlog` output only appears under `--verbose` and is the first thing to reach for when debugging a configuration.
47
+
`cli.main` wires the three halves together: `load_conf` produces the configuration, `get_env` turns it into an environment, and `run_cmd`/`run_shell` hand that environment to a shell. `--detect-backend` and `--reset-backend` return before any of it, since neither has a use for a configuration or a command. The command is an argparse `REMAINDER`, so everything from its first word onwards reaches it verbatim — including options keycmd has of its own — which leaves `end_of_options` to strip the `--` that argparse keeps in place, since only the first one is keycmd's to remove. Errors reach the user through `logs.error`, which exits with status 1 and takes the hint lines that go under the error with it; `logs.vlog` output only appears under `--verbose` and is the first thing to reach for when debugging a configuration.
48
48
49
49
**`conf.py` — where the configuration comes from.** Later sources win, merged deeply by `merge_conf`: defaults, then `~/.keycmd`, then every `.keycmd` found walking up from the working directory (outermost first), then the first `pyproject.toml` found walking up, whose `[tool.keycmd]` table is used. Both searches cover the same ground, so `load_conf` collects them in a single pass over `walk_up`, which stops at a `.git` directory, at the home folder, and at the root of the file system, so the walk never escapes a repository. `USERPROFILE` is a module attribute so tests can point the user config elsewhere. The merged result is `cast` to `Conf` rather than validated: it is user authored, and `get_env` reports violations as user errors.
50
50
@@ -54,7 +54,7 @@ The README is a landing page and nothing more: what it says about behaviour it s
54
54
55
55
**`wsl.py` — the boundary between WSL and Windows.** WSL users install keycmd on Windows, which leaves it a Windows process with a Windows idea of a shell. `from_wsl` decides whether it was called from a distro — a `wsl.exe`/`wslhost.exe` ancestor decides it, a Windows shell found first decides against it, and a UNC working directory settles the rest — after which `run_shell`/`run_cmd` hand the work to `wsl.exe` rather than to a Windows shell. A UNC working directory also names the distro, which `wsl_argv` passes as `--distribution` so that a second distro does not send the command to the default one. `KEYCMD_WSL` overrides that decision in either direction. Neither side of the boundary inherits the other's environment, so `share_env` lists the exposed variables in `WSLENV`, which is the only thing that crosses.
56
56
57
-
**`shell.py` — where the platform differences live.**`get_shell` asks shellingham which shell invoked the process and falls back to `$SHELL` or `%COMSPEC%`. `cmd` takes `/C` and keeps the command's arguments separate; every other shell takes `-c` and the single string `join_cmd` builds. One argument is already a command line — the form the README recommends — and is handed over as typed, so the shell interprets it; several arguments are an argv vector, and `quote` protects each so that the shell does not split them into words a second time. `quote` reaches for `shlex` for posix shells and doubles the quote for powershell, which also needs the call operator once its command name ends up quoted. `exec` replaces the process with `execvpe` on posix, but runs a subprocess on Windows, which has no equivalent; `USE_SUBPROCESS` and the `IS_WINDOWS`/`IS_POSIX` flags are module attributes so tests can drive both paths on either platform.
57
+
**`shell.py` — where the platform differences live.**`get_shell` asks shellingham which shell invoked the process and falls back to `$SHELL` or `%COMSPEC%`. `cmd` takes `/C` and keeps the command's arguments separate; every other shell takes `-c` and the single string `join_cmd` builds. Several arguments are an argv vector — the form the docs lead with, `keycmd npm install` — and `quote` protects each so that the shell does not split them into words a second time; one argument is already a command line, and is handed over as typed, so the shell interprets it. `quote` reaches for `shlex` for posix shells and doubles the quote for powershell, which also needs the call operator once its command name ends up quoted. `exec` replaces the process with `execvpe` on posix, but runs a subprocess on Windows, which has no equivalent; `USE_SUBPROCESS` and the `IS_WINDOWS`/`IS_POSIX` flags are module attributes so tests can drive both paths on either platform.
Copy file name to clipboardExpand all lines: docs/examples/azure-artifacts.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -95,7 +95,7 @@ always-auth=true
95
95
Now I can set up my `node_modules` just by calling:
96
96
97
97
```bash
98
-
keycmd 'npm install'
98
+
keycmd npm install
99
99
```
100
100
101
101
🚀
@@ -116,4 +116,4 @@ secrets:
116
116
environment: PAT_B64
117
117
```
118
118
119
-
When I call `keycmd 'docker compose build'` these two variables are exposed by keycmd and subsequently they are available as [docker compose build secrets](https://docs.docker.com/compose/use-secrets/). 👌
119
+
When I call `keycmd docker compose build` these two variables are exposed by keycmd and subsequently they are available as [docker compose build secrets](https://docs.docker.com/compose/use-secrets/). 👌
Copy file name to clipboardExpand all lines: docs/getting-started/quick-start.md
+13-3Lines changed: 13 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,7 +50,17 @@ This says: look up the credential `my-secret` for user `my-username`, and expose
50
50
51
51
## 3. Run a command
52
52
53
-
Open a terminal and run a command that prints the secret. That looks different depending on the shell you use:
53
+
Open a terminal and put `keycmd` in front of the command you want the secret to reach. That is all there is to it, and it is how you will use keycmd for real:
54
+
55
+
```bash
56
+
keycmd npm install
57
+
keycmd docker compose up
58
+
keycmd pytest
59
+
```
60
+
61
+
The tools you run read their credentials from the environment themselves, and keycmd is what puts them there.
62
+
63
+
To check your setup right now, print the secret instead. That is the one thing that *does* need quotes, since a variable written in your command line is expanded by your own shell — before keycmd has set it:
54
64
55
65
=== "bash / zsh"
56
66
@@ -78,11 +88,11 @@ You've successfully set up keycmd! 👏
78
88
79
89
keycmd read your configuration, looked `my-secret` up in your OS keyring, put the password in the environment as `SECRET`, and handed that environment to your shell along with your command. When the command finished, the variable went with it: your own shell never had it.
80
90
81
-
Note the quotes in the bash and PowerShell examples. Quoting the whole command as one argument is what lets *your command's shell* expand `$SECRET`, rather than your own shell expanding it before keycmd ever sees it. See [Running commands](../guide/running-commands.md) for the details.
91
+
Note the quotes in the bash and PowerShell examples. Quoting the whole command as one argument is what lets *your command's shell* expand `$SECRET`, rather than your own shell expanding it into nothing beforehand. (`cmd` is the exception: it leaves an undefined `%SECRET%` alone, so it survives the trip unquoted.) Commands that read the environment themselves — which is nearly all of them — need none of this. See [Running commands](../guide/running-commands.md) for the details.
82
92
83
93
## Where to go next
84
94
85
-
*[Running commands](../guide/running-commands.md) — the two ways to invoke keycmd, quoting, and subshells.
95
+
*[Running commands](../guide/running-commands.md) — prefixing a command, quoting one, and subshells.
86
96
*[Configuration](../guide/configuration.md) — where configuration lives, and everything you can put in it.
87
97
*[Examples](../examples/openai.md) — an OpenAI API key, and a real world setup where poetry, npm and docker compose share a single Azure DevOps token.
88
98
*[Troubleshooting](../guide/troubleshooting.md) — if any of the above did not go as planned, `keycmd --verbose` will tell you why.
Copy file name to clipboardExpand all lines: docs/guide/running-commands.md
+46-14Lines changed: 46 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,42 +3,74 @@
3
3
There are two ways to use keycmd:
4
4
5
5
```bash
6
-
keycmd 'your command'# run one command with the credentials exposed
6
+
keycmd your command# run one command with the credentials exposed
7
7
keycmd --shell # open a subshell with the credentials exposed
8
8
```
9
9
10
10
The first is the preferred one, since your secrets are only exposed as environment variables for the duration of a single command. The second is less preferable, but can be convenient when you are debugging a process that depends on the credentials you are exposing.
11
11
12
-
## One command
12
+
## Prefix your command
13
13
14
-
In its most common form, keycmd takes the command to run as a single quoted argument:
14
+
Write the command the way you would have written it anyway, and put `keycmd` in front of it:
15
15
16
16
```bash
17
-
keycmd 'npm install'
17
+
keycmd npm install
18
+
keycmd docker compose up -d
19
+
keycmd pytest -k auth --maxfail 1
18
20
```
19
21
20
-
Quoting the whole command as one argument is what lets you use your shell's syntax inside it:
22
+
That is the whole idea, and it is all most commands need: the tool you are running reads its credential from the environment itself, and keycmd is what puts it there.
23
+
24
+
Everything after `keycmd`'s own options belongs to your command, dashes and all. `keycmd pytest --verbose` runs pytest verbosely; it is `keycmd --verbose pytest` that makes *keycmd* verbose.
25
+
26
+
Each argument is passed on as the word it was, so nothing you typed is split or expanded a second time:
21
27
22
28
```bash
23
-
keycmd 'echo $SECRET | tr a-z A-Z'
29
+
# arrives as a single argument, spaces and all
30
+
keycmd mytool --message 'hello world'
24
31
```
25
32
26
-
keycmd hands that line to your shell exactly as you typed it, and your shell does the rest — pipes, redirects, variable expansion and all. This matters especially for the credentials themselves: `$SECRET` has to be expanded by the shell keycmd starts, because that is the only shell the variable exists in.
33
+
The quotes there are your own shell's, doing their usual job. keycmd re-quotes each argument for the shell it hands the command to, so what `mytool` receives is the argv your shell built.
34
+
35
+
!!! note "Two limits on Windows"
27
36
28
-
## Separate arguments
37
+
Both are the platform's rather than keycmd's, and no amount of quoting lifts either. `cmd` reaches a command through the Windows command line, which cannot hold a newline and which expands `%VAR%` inside an argument. Windows PowerShell drops an embedded `"` and an empty argument when it calls a native command; `pwsh` (PowerShell 7.3 and up) does not.
29
38
30
-
You can also write the command out as separate arguments, and then keycmd keeps them separate:
39
+
## Ending keycmd's options with `--`
40
+
41
+
A command whose *first* word starts with a dash would be read as an option of keycmd's. Put `--` in front of it to say that keycmd's own options have ended:
31
42
32
43
```bash
33
-
# arrives as a single argument, spaces and all
34
-
keycmd mytool --message 'hello world'
44
+
keycmd -- --my-oddly-named-tool
35
45
```
36
46
37
-
Since each argument is passed on as the word it was, your shell's syntax is *not* interpreted a second time in this form. If you want `$SECRET` expanded, either let your own shell expand it, or use the single argument form above.
47
+
`--` is also simply a habit worth keeping, since every tool that goes on to run another one takes it:
48
+
49
+
```bash
50
+
keycmd -- npm install
51
+
```
52
+
53
+
Only the `--` that ends keycmd's options is removed; any further `--` is your command's own and is passed along untouched.
54
+
55
+
## A quoted command line
56
+
57
+
Written as a single quoted argument, the command is handed to your shell as typed, and the shell interprets it — pipes, redirects, globs, `&&` and variable expansion included:
58
+
59
+
```bash
60
+
keycmd 'echo $SECRET | tr a-z A-Z'
61
+
keycmd 'npm ci && npm run build'
62
+
```
63
+
64
+
This matters especially for the credentials themselves. `$SECRET` has to be expanded by the shell keycmd starts, because that is the only shell the variable exists in:
65
+
66
+
```bash
67
+
keycmd echo$SECRET# your own shell expands it, before keycmd sets it — empty
68
+
keycmd 'echo $SECRET'# the shell keycmd starts expands it — correct
69
+
```
38
70
39
71
!!! tip "Which form should I use?"
40
72
41
-
Use the quoted form for anything that needs a shell: pipes, `&&`, redirects, globs, and above all the credentials you came here for. Use separate arguments when you are passing along text that must survive untouched, such as an argument that itself contains `$` or quotes.
73
+
Prefix your command as you normally write it, and reach for quotes when you need a shell: pipes, `&&`, redirects, globs, and above all a credential you want expanded into the command line rather than read from the environment.
42
74
43
75
## A subshell
44
76
@@ -52,7 +84,7 @@ Every command you run in it has the credentials available, until you exit it. Ke
52
84
53
85
## Which shell keycmd uses
54
86
55
-
keycmd asks [shellingham](https://github.com/sarugaku/shellingham) which shell invoked it, and falls back on `$SHELL` on posix or `%COMSPEC%` on Windows if that fails. In other words, it runs your command in the shell you were already using.
87
+
keycmd asks [shellingham](https://github.com/sarugaku/shellingham) which shell invoked it, and falls back on `$SHELL` on posix or `%COMSPEC%` on Windows if that fails. In other words, it runs your command in the shell you were already using — in both forms, which is what lets `keycmd npm install` find the `npm.cmd` on your Windows `PATH`.
56
88
57
89
On posix, keycmd replaces its own process with the shell (`execvpe`), so it does not sit in the process tree waiting around. Windows has no equivalent, so there keycmd runs the shell as a subprocess and passes its exit code along.
Copy file name to clipboardExpand all lines: docs/guide/troubleshooting.md
+10-2Lines changed: 10 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,7 +29,7 @@ That output answers, in order, the four questions a misbehaving run usually come
29
29
***Which backend answered?** See [keyring backends](keyring-backends.md).
30
30
***What was actually run?** Including the shell, and the exact argument vector handed to it.
31
31
32
-
The example above uses `cmd.exe`; in bash or PowerShell the command would be quoted as one argument, as in `keycmd -v 'echo $ARTIFACTS_TOKEN_B64'`.
32
+
The example above echoes a variable, which is why the command is written out for `cmd.exe` rather than prefixed the usual way; in bash or PowerShell it would be quoted as one argument, as in `keycmd -v 'echo $ARTIFACTS_TOKEN_B64'`. See [running commands](running-commands.md).
33
33
34
34
## Common problems
35
35
@@ -48,7 +48,15 @@ keycmd echo $SECRET # your shell expands $SECRET — before keycmd sets it
48
48
keycmd 'echo $SECRET'# the shell keycmd starts expands it — correct
49
49
```
50
50
51
-
Quote the whole command, so that the shell keycmd starts is the one interpreting it. See [running commands](running-commands.md).
51
+
Quote the whole command, so that the shell keycmd starts is the one interpreting it. This only comes up when you write the credential into the command line yourself; a tool that reads it from its own environment needs nothing but `keycmd` in front of it. See [running commands](running-commands.md).
52
+
53
+
### `sh: --: invalid option`, or `--: command not found`
54
+
55
+
Your keycmd is old enough to pass a leading `--` on to the shell as the first word of the command. Upgrade, or leave the `--` out — `keycmd npm install` works on every version.
56
+
57
+
### keycmd took my command's `--verbose` (or `--version`, or `-v`)
58
+
59
+
Only the options *before* your command are keycmd's; everything from the first word of the command onwards is passed on untouched. `keycmd --verbose pytest` makes keycmd verbose, `keycmd pytest --verbose` makes pytest verbose. If the command's own name starts with a dash, put `--` in front of it.
52
60
53
61
### `keycmd: error: keyring has no backend to read credentials from`
Copy file name to clipboardExpand all lines: docs/guide/wsl.md
+6-2Lines changed: 6 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,6 +30,10 @@ keycmd works out where it was called from by looking at its own process tree and
30
30
31
31
If you have more than one distribution installed, and you are working somewhere on the distribution's own file system, that working directory also names the distribution, and keycmd passes it to `wsl.exe` as `--distribution`, so your command goes to the distribution you are in rather than the default one.
32
32
33
+
!!! warning "Quotes do not survive the crossing"
34
+
35
+
`wsl.exe` strips the quotes from its own command line before the distribution's shell ever sees it, so an argument containing spaces arrives as several words no matter how it is written. `keycmd npm install` is unaffected, and so is anything else without spaces inside an argument; `keycmd mytool --message 'hello world'` is not, and there is nothing keycmd can do about it from the Windows side.
36
+
33
37
## Your credentials have to be told to cross
34
38
35
39
Your credentials do not come along by themselves, since neither side of the WSL boundary inherits the other's environment. Only the variables listed in [`WSLENV`](https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows/) make the trip, so keycmd adds the variables from your configuration to it. Anything you had already listed in `WSLENV` yourself is kept.
@@ -39,8 +43,8 @@ Your credentials do not come along by themselves, since neither side of the WSL
39
43
Set the `KEYCMD_WSL` environment variable to override the decision in either direction:
40
44
41
45
```bash
42
-
KEYCMD_WSL=0 keycmd 'echo $SECRET'# stay on the windows side
43
-
KEYCMD_WSL=1 keycmd 'echo $SECRET'# go through wsl.exe regardless
46
+
KEYCMD_WSL=0 keycmd npm install# stay on the windows side
47
+
KEYCMD_WSL=1 keycmd npm install# go through wsl.exe regardless
44
48
```
45
49
46
50
`keycmd --verbose` reports which way it went, and what it based that on:
0 commit comments