diff --git a/coder-sdk/env.go b/coder-sdk/env.go
index 2eced5f1..a95ed4bf 100644
--- a/coder-sdk/env.go
+++ b/coder-sdk/env.go
@@ -39,10 +39,6 @@ type RebuildMessage struct {
 	Text             string           `json:"text"`
 	Required         bool             `json:"required"`
 	AutoOffThreshold xjson.MSDuration `json:"auto_off_threshold" tab:"-"`
-	RebuildMessages  []struct {
-		Text     string `json:"text"`
-		Required bool   `json:"required"`
-	} `json:"rebuild_messages" tab:"-"`
 }
 
 // EnvironmentStat represents the state of an environment
diff --git a/docs/coder_sh.md b/docs/coder_sh.md
index 8bd80ac6..72270e8f 100644
--- a/docs/coder_sh.md
+++ b/docs/coder_sh.md
@@ -14,6 +14,7 @@ coder sh [environment_name] [<command [args...]>] [flags]
 
 ```
 coder sh backend-env
+coder sh front-end-dev cat ~/config.json
 ```
 
 ### Options
diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go
index d00796cd..c629beb3 100644
--- a/internal/cmd/cmd.go
+++ b/internal/cmd/cmd.go
@@ -21,24 +21,24 @@ func Make() *cobra.Command {
 	}
 
 	app.AddCommand(
-		makeLoginCmd(),
-		makeLogoutCmd(),
-		makeShellCmd(),
-		makeUsersCmd(),
-		makeConfigSSHCmd(),
-		makeSecretsCmd(),
-		envsCommand(),
-		makeSyncCmd(),
-		makeURLCmd(),
+		loginCmd(),
+		logoutCmd(),
+		shCmd(),
+		usersCmd(),
+		configSSHCmd(),
+		secretsCmd(),
+		envsCmd(),
+		syncCmd(),
+		urlCmd(),
 		resourceCmd(),
-		completionCmd,
-		genDocs(app),
+		completionCmd(),
+		genDocsCmd(app),
 	)
 	app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output")
 	return app
 }
 
-func genDocs(rootCmd *cobra.Command) *cobra.Command {
+func genDocsCmd(rootCmd *cobra.Command) *cobra.Command {
 	return &cobra.Command{
 		Use:     "gen-docs [dir_path]",
 		Short:   "Generate a markdown documentation tree for the root command.",
@@ -52,17 +52,18 @@ func genDocs(rootCmd *cobra.Command) *cobra.Command {
 }
 
 // reference: https://github.com/spf13/cobra/blob/master/shell_completions.md
-var completionCmd = &cobra.Command{
-	Use:   "completion [bash|zsh|fish|powershell]",
-	Short: "Generate completion script",
-	Example: `coder completion fish > ~/.config/fish/completions/coder.fish
+func completionCmd() *cobra.Command {
+	return &cobra.Command{
+		Use:   "completion [bash|zsh|fish|powershell]",
+		Short: "Generate completion script",
+		Example: `coder completion fish > ~/.config/fish/completions/coder.fish
 coder completion zsh > "${fpath[1]}/_coder"
 
 Linux:
   $ coder completion bash > /etc/bash_completion.d/coder
 MacOS:
   $ coder completion bash > /usr/local/etc/bash_completion.d/coder`,
-	Long: `To load completions:
+		Long: `To load completions:
 
 Bash:
 
@@ -93,19 +94,20 @@ $ coder completion fish | source
 To load completions for each session, execute once:
 $ coder completion fish > ~/.config/fish/completions/coder.fish
 `,
-	DisableFlagsInUseLine: true,
-	ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
-	Args:                  cobra.ExactValidArgs(1),
-	Run: func(cmd *cobra.Command, args []string) {
-		switch args[0] {
-		case "bash":
-			_ = cmd.Root().GenBashCompletion(os.Stdout) // Best effort.
-		case "zsh":
-			_ = cmd.Root().GenZshCompletion(os.Stdout) // Best effort.
-		case "fish":
-			_ = cmd.Root().GenFishCompletion(os.Stdout, true) // Best effort.
-		case "powershell":
-			_ = cmd.Root().GenPowerShellCompletion(os.Stdout) // Best effort.
-		}
-	},
+		DisableFlagsInUseLine: true,
+		ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
+		Args:                  cobra.ExactValidArgs(1),
+		Run: func(cmd *cobra.Command, args []string) {
+			switch args[0] {
+			case "bash":
+				_ = cmd.Root().GenBashCompletion(os.Stdout) // Best effort.
+			case "zsh":
+				_ = cmd.Root().GenZshCompletion(os.Stdout) // Best effort.
+			case "fish":
+				_ = cmd.Root().GenFishCompletion(os.Stdout, true) // Best effort.
+			case "powershell":
+				_ = cmd.Root().GenPowerShellCompletion(os.Stdout) // Best effort.
+			}
+		},
+	}
 }
diff --git a/internal/cmd/configssh.go b/internal/cmd/configssh.go
index db76b71e..38d20fdf 100644
--- a/internal/cmd/configssh.go
+++ b/internal/cmd/configssh.go
@@ -18,7 +18,7 @@ import (
 	"golang.org/x/xerrors"
 )
 
-func makeConfigSSHCmd() *cobra.Command {
+func configSSHCmd() *cobra.Command {
 	var (
 		configpath string
 		remove     = false
diff --git a/internal/cmd/envs.go b/internal/cmd/envs.go
index 67c6b50e..1ddf9309 100644
--- a/internal/cmd/envs.go
+++ b/internal/cmd/envs.go
@@ -17,7 +17,7 @@ import (
 
 const defaultImgTag = "latest"
 
-func envsCommand() *cobra.Command {
+func envsCmd() *cobra.Command {
 	var user string
 	cmd := &cobra.Command{
 		Use:   "envs",
@@ -28,12 +28,12 @@ func envsCommand() *cobra.Command {
 
 	cmd.AddCommand(
 		lsEnvsCommand(&user),
-		stopEnvsCommand(&user),
-		rmEnvsCommand(&user),
+		stopEnvsCmd(&user),
+		rmEnvsCmd(&user),
 		watchBuildLogCommand(&user),
 		rebuildEnvCommand(&user),
-		createEnvCommand(&user),
-		editEnvCommand(&user),
+		createEnvCmd(&user),
+		editEnvCmd(&user),
 	)
 	return cmd
 }
@@ -84,7 +84,7 @@ func lsEnvsCommand(user *string) *cobra.Command {
 	return cmd
 }
 
-func stopEnvsCommand(user *string) *cobra.Command {
+func stopEnvsCmd(user *string) *cobra.Command {
 	return &cobra.Command{
 		Use:   "stop [...environment_names]",
 		Short: "stop Coder environments by name",
@@ -131,7 +131,7 @@ coder envs --user charlie@coder.com ls -o json \
 	}
 }
 
-func createEnvCommand(user *string) *cobra.Command {
+func createEnvCmd(user *string) *cobra.Command {
 	var (
 		org    string
 		img    string
@@ -239,7 +239,7 @@ coder envs create --cpu 4 --disk 100 --memory 8 --image 5f443b16-30652892427b955
 	return cmd
 }
 
-func editEnvCommand(user *string) *cobra.Command {
+func editEnvCmd(user *string) *cobra.Command {
 	var (
 		org      string
 		img      string
@@ -336,7 +336,7 @@ coder envs edit back-end-env --disk 20`,
 	return cmd
 }
 
-func rmEnvsCommand(user *string) *cobra.Command {
+func rmEnvsCmd(user *string) *cobra.Command {
 	var force bool
 	cmd := &cobra.Command{
 		Use:   "rm [...environment_names]",
diff --git a/internal/cmd/login.go b/internal/cmd/login.go
index 2984822b..d6a8db11 100644
--- a/internal/cmd/login.go
+++ b/internal/cmd/login.go
@@ -18,7 +18,7 @@ import (
 	"golang.org/x/xerrors"
 )
 
-func makeLoginCmd() *cobra.Command {
+func loginCmd() *cobra.Command {
 	return &cobra.Command{
 		Use:   "login [Coder Enterprise URL eg. https://my.coder.domain/]",
 		Short: "Authenticate this client for future operations",
diff --git a/internal/cmd/logout.go b/internal/cmd/logout.go
index b653d68f..dde3d758 100644
--- a/internal/cmd/logout.go
+++ b/internal/cmd/logout.go
@@ -9,7 +9,7 @@ import (
 	"golang.org/x/xerrors"
 )
 
-func makeLogoutCmd() *cobra.Command {
+func logoutCmd() *cobra.Command {
 	return &cobra.Command{
 		Use:   "logout",
 		Short: "Remove local authentication credentials if any exist",
diff --git a/internal/cmd/secrets.go b/internal/cmd/secrets.go
index 879d6bde..baa8ad25 100644
--- a/internal/cmd/secrets.go
+++ b/internal/cmd/secrets.go
@@ -14,7 +14,7 @@ import (
 	"cdr.dev/coder-cli/internal/x/xtabwriter"
 )
 
-func makeSecretsCmd() *cobra.Command {
+func secretsCmd() *cobra.Command {
 	var user string
 	cmd := &cobra.Command{
 		Use:   "secrets",
@@ -26,28 +26,28 @@ func makeSecretsCmd() *cobra.Command {
 		&cobra.Command{
 			Use:   "ls",
 			Short: "List all secrets owned by the active user",
-			RunE:  listSecrets(&user),
+			RunE:  listSecretsCmd(&user),
 		},
-		makeCreateSecret(&user),
+		createSecretCmd(&user),
 		&cobra.Command{
 			Use:     "rm [...secret_name]",
 			Short:   "Remove one or more secrets by name",
 			Args:    cobra.MinimumNArgs(1),
-			RunE:    makeRemoveSecrets(&user),
+			RunE:    removeSecretsCmd(&user),
 			Example: "coder secrets rm mysql-password mysql-user",
 		},
 		&cobra.Command{
 			Use:     "view [secret_name]",
 			Short:   "View a secret by name",
 			Args:    cobra.ExactArgs(1),
-			RunE:    makeViewSecret(&user),
+			RunE:    viewSecretCmd(&user),
 			Example: "coder secrets view mysql-password",
 		},
 	)
 	return cmd
 }
 
-func makeCreateSecret(userEmail *string) *cobra.Command {
+func createSecretCmd(userEmail *string) *cobra.Command {
 	var (
 		fromFile    string
 		fromLiteral string
@@ -136,7 +136,7 @@ coder secrets create aws-credentials --from-file ./credentials.json`,
 	return cmd
 }
 
-func listSecrets(userEmail *string) func(cmd *cobra.Command, _ []string) error {
+func listSecretsCmd(userEmail *string) func(cmd *cobra.Command, _ []string) error {
 	return func(cmd *cobra.Command, _ []string) error {
 		client, err := newClient()
 		if err != nil {
@@ -169,7 +169,7 @@ func listSecrets(userEmail *string) func(cmd *cobra.Command, _ []string) error {
 	}
 }
 
-func makeViewSecret(userEmail *string) func(cmd *cobra.Command, args []string) error {
+func viewSecretCmd(userEmail *string) func(cmd *cobra.Command, args []string) error {
 	return func(cmd *cobra.Command, args []string) error {
 		var (
 			name = args[0]
@@ -196,7 +196,7 @@ func makeViewSecret(userEmail *string) func(cmd *cobra.Command, args []string) e
 	}
 }
 
-func makeRemoveSecrets(userEmail *string) func(c *cobra.Command, args []string) error {
+func removeSecretsCmd(userEmail *string) func(c *cobra.Command, args []string) error {
 	return func(cmd *cobra.Command, args []string) error {
 		client, err := newClient()
 		if err != nil {
diff --git a/internal/cmd/shell.go b/internal/cmd/shell.go
index 18197570..9d4b9b10 100644
--- a/internal/cmd/shell.go
+++ b/internal/cmd/shell.go
@@ -23,11 +23,12 @@ import (
 
 func getEnvsForCompletion(user string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		ctx := cmd.Context()
 		client, err := newClient()
 		if err != nil {
 			return nil, cobra.ShellCompDirectiveDefault
 		}
-		envs, err := getEnvs(context.TODO(), client, user)
+		envs, err := getEnvs(ctx, client, user)
 		if err != nil {
 			return nil, cobra.ShellCompDirectiveDefault
 		}
@@ -40,7 +41,7 @@ func getEnvsForCompletion(user string) func(cmd *cobra.Command, args []string, t
 	}
 }
 
-func makeShellCmd() *cobra.Command {
+func shCmd() *cobra.Command {
 	return &cobra.Command{
 		Use:                "sh [environment_name] [<command [args...]>]",
 		Short:              "Open a shell and execute commands in a Coder environment",
@@ -49,13 +50,13 @@ func makeShellCmd() *cobra.Command {
 		DisableFlagParsing: true,
 		ValidArgsFunction:  getEnvsForCompletion(coder.Me),
 		RunE:               shell,
-		Example:            "coder sh backend-env",
+		Example: `coder sh backend-env
+coder sh front-end-dev cat ~/config.json`,
 	}
 }
 
-func shell(_ *cobra.Command, cmdArgs []string) error {
-	ctx := context.Background()
-
+func shell(cmd *cobra.Command, cmdArgs []string) error {
+	ctx := cmd.Context()
 	command := "sh"
 	args := []string{"-c"}
 	if len(cmdArgs) > 1 {
@@ -106,6 +107,18 @@ func runCommand(ctx context.Context, envName, command string, args []string) err
 		return xerrors.Errorf("find environment: %w", err)
 	}
 
+	// check if a rebuild is required before attempting to open a shell
+	for _, r := range env.RebuildMessages {
+		// use the first rebuild message that is required
+		if r.Required {
+			return clog.Error(
+				fmt.Sprintf(`environment "%s" requires a rebuild`, env.Name),
+				clog.Causef(r.Text), clog.BlankLine,
+				clog.Tipf(`run "coder envs rebuild %s" to rebuild`, env.Name),
+			)
+		}
+	}
+
 	termFD := os.Stdout.Fd()
 
 	isInteractive := terminal.IsTerminal(int(termFD))
diff --git a/internal/cmd/sync.go b/internal/cmd/sync.go
index 4ff3f4ec..7247d883 100644
--- a/internal/cmd/sync.go
+++ b/internal/cmd/sync.go
@@ -15,7 +15,7 @@ import (
 	"golang.org/x/xerrors"
 )
 
-func makeSyncCmd() *cobra.Command {
+func syncCmd() *cobra.Command {
 	var init bool
 	cmd := &cobra.Command{
 		Use:   "sync [local directory] [<env name>:<remote directory>]",
diff --git a/internal/cmd/urls.go b/internal/cmd/urls.go
index c91427da..1faa1328 100644
--- a/internal/cmd/urls.go
+++ b/internal/cmd/urls.go
@@ -18,7 +18,7 @@ import (
 	"cdr.dev/coder-cli/internal/x/xtabwriter"
 )
 
-func makeURLCmd() *cobra.Command {
+func urlCmd() *cobra.Command {
 	var outputFmt string
 	cmd := &cobra.Command{
 		Use:   "urls",
@@ -29,7 +29,7 @@ func makeURLCmd() *cobra.Command {
 		Short:             "List all DevURLs for an environment",
 		Args:              cobra.ExactArgs(1),
 		ValidArgsFunction: getEnvsForCompletion(coder.Me),
-		RunE:              makeListDevURLs(&outputFmt),
+		RunE:              listDevURLsCmd(&outputFmt),
 	}
 	lsCmd.Flags().StringVarP(&outputFmt, "output", "o", "human", "human|json")
 
@@ -43,7 +43,7 @@ func makeURLCmd() *cobra.Command {
 	cmd.AddCommand(
 		lsCmd,
 		rmCmd,
-		makeCreateDevURL(),
+		createDevURLCmd(),
 	)
 
 	return cmd
@@ -89,7 +89,7 @@ func accessLevelIsValid(level string) bool {
 
 // Run gets the list of active devURLs from the cemanager for the
 // specified environment and outputs info to stdout.
-func makeListDevURLs(outputFmt *string) func(cmd *cobra.Command, args []string) error {
+func listDevURLsCmd(outputFmt *string) func(cmd *cobra.Command, args []string) error {
 	return func(cmd *cobra.Command, args []string) error {
 		envName := args[0]
 		devURLs, err := urlList(cmd.Context(), envName)
@@ -120,7 +120,7 @@ func makeListDevURLs(outputFmt *string) func(cmd *cobra.Command, args []string)
 	}
 }
 
-func makeCreateDevURL() *cobra.Command {
+func createDevURLCmd() *cobra.Command {
 	var (
 		access  string
 		urlname string
diff --git a/internal/cmd/users.go b/internal/cmd/users.go
index b74b1758..15dfbf78 100644
--- a/internal/cmd/users.go
+++ b/internal/cmd/users.go
@@ -10,7 +10,7 @@ import (
 	"cdr.dev/coder-cli/internal/x/xtabwriter"
 )
 
-func makeUsersCmd() *cobra.Command {
+func usersCmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "users",
 		Short: "Interact with Coder user accounts",
