Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.36.0] - 2026-09-09

### Fixed

- **Editing prose under `.devcontainer/` no longer throws away the prebuilt
Expand Down Expand Up @@ -41,6 +43,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
context, so this commit moves the tag once; launches build locally until
`devcontainer-prebuild.yml` republishes on `main`.

- **`dl <ws> -- <command>` no longer re-splits a quoted argument, and no longer
runs one as shell.** The words after `--` were rejoined with plain spaces and
handed to `bash -lc` as a command line, so every space the host's shell had
already consumed became a separator again. `dl <ws> -- claude 'fix the bug'`
arrived as four arguments where one was meant. Each word is quoted now
(`shell::join`), so the remote argv is the argv that was typed.

Two consequences beyond the splitting, both of them silent. A word holding `#`
commented out the rest of the line: a supervisor sending
`claude 'Address the open review on PR #10848 (...)'` reached the agent as the
single word `Address`, and everything after the `#` -- the whole prompt,
including every rule it carried -- was discarded by the remote shell before
`claude` ran. And a word holding `$(...)` or a backtick was *executed*, in a
workspace that has the forwarded `GH_TOKEN`, which made any text flowing into
a `dl --` command line (a PR title, a review body) shell code.

`aid` composed its own line and passed it as one word, which survived only
because the rejoin was an identity on a single argument. It hands dl argv now
and quotes nothing itself. The composed payload is byte for byte what it was:
a bare `NAME=value` needs no quoting, so the assignment-prefix spelling the
README documents still reaches the shell as one.

A shell snippet is still asked for by naming a shell -- `dl <ws> -- bash -lc
'a && b'` -- and that spelling was broken before this too, running `bash -lc a`
and then `b`. Passing a snippet as a single word no longer works, because a
single word is now a program name: `dl <ws> -- 'exit 7'` looks for a program
called `exit 7` and exits 127 where it used to exit 7. The e2e probes were the
only callers in the tree spelling it that way, and they name `bash -lc` now.
`dl <ws> -- ""` went from exit 0 to 127 for the same reason.

## [0.35.0] - 2026-09-09

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ one argument instead of a clone, a config file and a build command.
[![GitHub pull-requests merged](https://badgen.net/github/merged-prs/blooop/devlaunch)](https://github.com/blooop/devlaunch/pulls?q=is%3Amerged)
[![GitHub release](https://img.shields.io/github/release/blooop/devlaunch.svg)](https://GitHub.com/blooop/devlaunch/releases/)
[![PyPI](https://img.shields.io/pypi/v/devlaunch)](https://pypi.org/project/devlaunch/)
[![Conda](https://img.shields.io/badge/conda-v0.35.0-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch)
[![Conda](https://img.shields.io/badge/conda-v0.36.0-brightgreen?logo=anaconda)](https://prefix.dev/channels/blooop/packages/devlaunch)
[![License](https://img.shields.io/github/license/blooop/devlaunch)](https://opensource.org/license/mit/)
[![Platform](https://img.shields.io/badge/platform-linux--64-blue)](https://github.com/blooop/devlaunch/releases)
[![Pixi Badge](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/prefix-dev/pixi/main/assets/badge/v0.json)](https://pixi.sh)
Expand Down Expand Up @@ -276,7 +276,7 @@ clone, and [docs/cleanup.md](docs/cleanup.md) says what it carries one past and

```bash
$ dl --version
dl 0.35.0
dl 0.36.0
```

`--devcontainer <variant|path>` picks a non-default `devcontainer.json`. A bare name means
Expand Down
31 changes: 31 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,37 @@ that clone last fetched, the attach says how far behind before it hands over the
shell. [How fresh a launch is](workspaces.md#how-fresh-a-launch-is) is the whole
of the freshness rules, and the section under it names which verb moves what.

## What `--` takes

The command and its arguments, one word each. `dl` quotes every word on the way
into the remote payload, so a quoted argument stays one argument: `dl <ws> --
claude 'fix the bug'` runs `claude` with a single argument, and a word holding a
space, a `#`, a `$(...)` or a backtick is that word rather than shell syntax.

It has to be built that way because the payload is one `bash -lc <line>` for both
transports. The words used to be rejoined with plain spaces, which gave the remote
shell back every separator your own shell had already consumed: quoted arguments
were re-split, a `#` commented out the rest of the line, and a `$(...)` ran.

One exception, and it is deliberate. The quoting leaves a word alone when it needs
none, and `=` counts as needing none, so a leading `NAME=value` still reaches the
shell as an assignment prefix and sets that variable for that command only. That
is what makes `dl <ws> -- IS_SANDBOX=1 claude ...` work, which is the spelling
`aid` uses and the one the README shows.

The exception ends where the quoting begins, and it ends abruptly. It is the
whole word that has to need no quoting, value included, so `FOO=bar` is an
assignment and `FOO='a b'` is not: the value's space makes the word
`'FOO=a b'`, and a shell reads a quoted word as a program name, so the command
exits 127 with the variable never set. Values made of `[A-Za-z0-9_@%+=:,./-]`
are the ones that survive. For anything else, name the shell and write the
assignment inside it: `dl <ws> -- bash -lc 'FOO="a b" cmd'`.

A shell snippet is a command like any other, so name the shell: `dl <ws> -- bash
-lc 'a && b'`. Redirections and pipes typed on your own command line belong to
your own shell and never reach `dl`, which is what makes `dl <ws> -- ls >
files.txt` write the file here.

## Commands that need a terminal

`dl <ws> -- <command>` gives the command a terminal whenever `dl` itself has one,
Expand Down
10 changes: 5 additions & 5 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ members = [
# The single source of the version (docs/rust-rewrite-plan.md: cutover ships
# 0.1.0, version read from Cargo.toml).
[workspace.package]
version = "0.35.0"
version = "0.36.0"
edition = "2024"
license = "MIT"
repository = "https://github.com/blooop/devlaunch"
Expand Down
Loading
Loading