Posted on

VSCode has documentation for persisting .bash_history in devcontainers.

But what about zsh users? How do we persist .zsh_history in devcontainers?

This post walks through how I did it.

Explanation

Familiarize yourself with the short docs for persisting .bash_history. We are going to use the same methodology but swap out the bash specific commands with zsh equivalents.

First change .bash_history --> .zsh_history and .bashrc --> .zshrc, but also we need to change the SNIPPET.

# The VSCode docs for bash say to use the following SNIPPET.
SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history"

# We'll use this SNIPPET for zsh
SNIPPET="autoload -Uz add-zsh-hook; append_history() { fc -W }; add-zsh-hook precmd append_history; export HISTFILE=/commandhistory/.zsh_history"

Here's what's happening.

PROMPT_COMMAND is a bash specific environment variable that is executed before the prompt (shell) is displayed. Zsh achieves the same functionality with hooks. In this case we use the precmd hook. Zsh has other hooks too (see precmd and chpwd).

The zsh hook takes a function. Hence the append_history() { fc -W } function. The fc -W is equivalent to the history -a from the bash commmand. It writes the history to the file specified by HISTFILE.

HISTFILE is common between bash and zsh. It tells the shell where to write/read the history. The shells default to ~/.bash_history and ~/.zsh_history respectively.

In short the SNIPPET is saying when a shell session is opened first set the new location of the .zsh_history file.

Putting It Together

My solution comes out looking like the below.

# Dockerfile

...

ARG USERNAME=vscode

RUN SNIPPET="autoload -Uz add-zsh-hook; append_history() { fc -W }; add-zsh-hook precmd append_history; export HISTFILE=/commandhistory/.zsh_history" \
    && mkdir /commandhistory \
    && touch /commandhistory/.zsh_history \
    && chown -R $USERNAME /commandhistory \
    && echo "$SNIPPET" >> "/home/$USERNAME/.zshrc"

...

// devcontainer.json

...

 "mounts": [
  "source=devcontainer-zshhistory,target=/commandhistory,type=volume"
 ],

...

Another Problem Emerges. Dotfile Managers

Are you using dotfiles in your devcontainer? If you are using Chezmoi or another dotfile manager, you may have noticed that the SNIPPET above does not take effect because the dotfile manager is overriding the .zshrc file that the SNIPPET was appeneded to.

The Solution:

I solved this by moving the SNIPPET to a postCreateCommand in the devcontainer.json file. In the end my solution looks like the below. The Dockerfile commands moved to a post_create.sh script to be called by the postCreateCommand.

// devcontainer.json

...

 "mounts": [
  "source=devcontainer-zshhistory,target=/commandhistory,type=volume"
 ],

...

 "postCreateCommand": "bash .devcontainer/post_create.sh",

...

# post_create.sh

#!/usr/bin/env bash
set -euxo pipefail

mkdir -p /commandhistory
touch /commandhistory/.zsh_history
chown -R ${USER} /commandhistory

echo "autoload -Uz add-zsh-hook; append_history() { fc -W }; add-zsh-hook precmd append_history; export HISTFILE=/commandhistory/.zsh_history" >> /home/${USER}/.zshrc