Ken Muse

Getting User Input When Starting a Dev Container

Did you know that dev containers (including Codespaces) can prompt users for input when the container is starting? Technically, I should say it can prompt when it is attaching. Every dev container supports a set of lifecycle hooks which allow code to be run at specific points during the container build process. These hooks are also available to Features, allowing those to also contribute code and behaviors. There are six available lifecycle script hooks:

HookPhaseDescriptionRuns onUser dataTool access
initializeCommandCreateRuns before the container is createdHostNoNo
onCreateCommandCreateFirst start of containerContainerNoNo
updateContentCommandCreate/RefreshRefreshes container contentContainerRepository/org secrets (cloud)No
postCreateCommandCreateFinalizes setup after assigned to userContainerUser secrets/permissions (cloud)No
postStartCommandExecuteRuns when container started by toolContainerTool-providedSometimes
postAttachCommandExecuteRuns after tool has connected to containerContainerTool-providedYes

One of those – postAttachCommand carries a special distinction. It’s raised once a tool — such as the IDE — has connected to the dev container. At this point, a user can interact with the container from that tool. Consequently, it’s one of the only commands that consistently allows interactions with users. There are a few caveats:

  • This works for VS Code and GitHub Codespaces.
  • At the moment, JetBrains does not display the running commands or allow them to interact with the user. Their implementation is still in Beta, so it also has a few other documented limitations, such as not supporting customizations.
  • The postStartCommand is displayed in the terminal by Codespaces and VS Code, but only a local VS Code dev container supports interactions from this lifecycle script.
  • In a local instance of VS Code, many of the events have access to the user. In fact, users open bug reports if that support disappears. I recommend treating this as an implementation detail if you want maximum portability. Since it’s local (and not cloud-based), it has a user context throughout the entire life of the container. Environments such as Codespaces do not, especially if you’re using their Prebuild support (which create/update containers without a connected user).

To get user input, we can call commands like read (in Bash). For Codespaces, this is very straightforward; it just works. For Visual Studio Code, we have a minor challenge – the code may be invoked in a way that prevents us from interacting fully. This can happen because the commands default to being invoked using the container’s shell. To avoid this, we have two options. First, we can use the array syntax to invoke a command instead of the shell-executed syntax. For example:

1// Use this 
2"postAttachCommand": ["bash", "-i", "-c", "read -p 'Type a message: ' -t 10 && echo Attach $REPLY"],
4// This may be executed using sh and can fail with a local VS Code
5"postAttachCommand": "read -p 'Type a message: ' -t 10 && echo You wrote $REPLY",

There is, however, a second, more reliable approach to solving this problem that works with both VS Code and Codespaces: scripts. When a script is invoked, the shebang for the script can change the execution environment. To work across more containers, start the script with #!/usr/bin/env bash. Hard-coding a path to Bash does not work well since it makes the script dependent on a single path, since Bash can be in different locations depending on the environment. The devcontainer.json then simply invokes the script. You’ll notice that the pattern of relying on scripts is used for organizing many of the lifecycle scripts. It makes it easier to develop and test the scripts from within a container. It also makes the scripts available to invoke when using the dev container.

1// Prefer using a script.
2// It doesn't have to be in the .devcontainer folder!
3"postAttachCommand":  ".devcontainer/"

And the script can specify the appropriate interpreter:

1#!/usr/bin/env bash
3read -t 10 -p "Type a message: " && echo You wrote "$REPLY"

The result looks like this (in VS Code)

1Running the postAttachCommand from devcontainer.json...
3[5396 ms] Start: Run in container: /bin/sh -c .devcontainer/
4Type a message: Hello world!
5You wrote "Hello world!"
6Done. Press any key to close the terminal.

One last tip. You may notice that I’m using -t 10 with my read commands. This allows me 10 seconds to enter a value before the command exits. This isn’t required, but it can be helpful if you’re running these scripts as part of other lifecycle events that may not allow user input. It ensures that the script doesn’t block the process waiting on user input from a non-interactive environment.

In my case, I’m using it to ensure that the process doesn’t freeze in JetBrains IntelliJ. Today, it doesn’t provide any interaction or a terminal with the outputs during the startup process. As a result, I wanted to have a graceful way of letting that step end if there’s no user input in a reasonable amount of time.

There is a request for VS Code to accept user input when starting a container. There’s also a broader request to formalize the support for user input in Dev Containers in VS Code. This could eventually leave to improvements in the specification that make this process more consistent across hosts and environments. Until then, there is a way to interact with users during the attachment process that works consistently across multiple implementations.