Atuin

Why Didn’t I Know About this Sooner?

If you spend most of your time in graphical user interfaces (GUIs), you can probably skip this post. However, if you spend a lot of time in terminal windows and would like a way to back up, sync, and search your command line history across all of the hosts you access, make sure you check out Atuin.

This all started when I had a few spare hours on a train ride and wanted to write a post about some recent network troubleshooting I was doing to figure out a Podman Macvlan issue. I didn’t have access to the computer I had done the work from and was having a hard time remembering the specific commands involved. My first thought was, “Why don’t I have a backup of my CLI history?” All of my source code is in Git, my docs and files are synced between computers with Syncthing, and my photos are all stored in Immich. How do I not have something similar for my CLI history?

My next thought was, “Maybe I can build something to store/sync it,” which was quickly followed by “maybe someone else has already built something.” It only took a few minutes to find Atuin and realize someone else had indeed built exactly what I was thinking but even better.

Key Atuin Features

The key things I wanted included:

  • nice integration with the shells I use - zsh and bash
  • end-to-end encrypted sync
  • easy search of history
  • self-hosted server support

Atuin took this further with:

  • adjustable ctrl-r toggle to filter for commands from all hosts, local host, current directory, etc
  • easily viewable ctrl-r history list
  • import of prior history
  • up-arrow history search (I found this was hard to adjust to and I disabled it)

Setting up an Atuin Server with Ansible

You can use the central server the project provides or you can self-host. Here are some excerpts from the Ansible playbook I used to set up the self-hosted server:

- set_fact:
    service: atuin

- name: Create config dir
  become: true
  file:
    path: "/opt/{{ service }}/data/config"
    state: directory
    owner: 1000
    group: 1000

- name: copy systemd container
  become: true
  copy:
    src: "{{ item }}"
    dest: "/etc/containers/systemd/{{ item }}"
    mode: "0644"
  with_items:
    - "{{ service }}.container"
  notify: restart atuin

- name: start service
  become: true
  systemd:
    enabled: true
    state: started
    name: "{{ service }}"
    daemon_reload: yes

I’m using Podman Quadlets with a Traefik frontend. I use Traefik for TLS and container routing, but you can also skip that part by pointing directly to the Atuin port or use something equivalent.

[Unit]
Description=Atuin Container
Requires=traefik-network.service
After=traefik-network.service

[Container]
AutoUpdate=registry
ContainerName=atuin
Image=ghcr.io/atuinsh/atuin:18.15
Exec=start
Network=traefik.network
Volume=/opt/atuin/data/config:/config
EnvironmentFile=/opt/atuin/.env
Environment=ATUIN_HOST="0.0.0.0"
Environment=ATUIN_OPEN_REGISTRATION="true"
Environment=ATUIN_DB_URI="sqlite:///config/atuin.db"
Environment=RUST_LOG=info,atuin_server=debug
Label=traefik.enable="True"
Label=traefik.docker.network=traefik
Label=traefik.http.routers.atuin.rule="Host(`atuin.example.com`)"
Label=traefik.http.routers.atuin.entrypoints="websecure"
Label=traefik.http.services.atuin.loadbalancer.server.port="8888"

[Install]
WantedBy=multi-user.target

Atuin Shell Config on Hosts

I have 6 “servers” and 3 “clients” on which I regularly use the CLI, including both x86_64 and aarch64 hosts. This Ansible playbook installs the Atuin client to sync their shell history with the central server.

- name: Set vars
  set_fact:
    atuin_version: "18.15.2"
    atuin_checksum: "sha256:97391908ceaf8cd2c74e2175ee01741a80ecf620b45131d34c7fe0f1ae925b91"
    atuin_unzip_dir: "atuin-{{ ansible_facts.architecture }}-unknown-linux-gnu"

- set_fact:
    atuin_checksum: "sha256:857b4918f1019b6e9efe23f16c5231b6a61f09452030e93e027423f862ffd944"
  when: ansible_facts.architecture == "aarch64"

- name: Set install info from version
  set_fact:
    atuin_install_path: "/opt/atuin/atuin-{{ atuin_version }}"
    atuin_url: "https://github.com/atuinsh/atuin/releases/download/v{{ atuin_version }}/atuin-{{ ansible_facts.architecture }}-unknown-linux-gnu.tar.gz"

- name: Create app dir
  become: true
  file:
    path: /opt/atuin
    state: directory

- name: Download
  get_url:
    url: "{{ atuin_url }}"
    dest: "/tmp/atuin-{{ atuin_version }}.tar.gz"
    checksum: "{{ atuin_checksum }}"

- name: Unzip Atuin
  unarchive:
    src: "/tmp/atuin-{{ atuin_version }}.tar.gz"
    dest: "/tmp"
    creates: "{{ atuin_install_path }}"
    remote_src: true
  register: atuin_unzip

- name: Move to proper location
  become: true
  command: "mv /tmp/{{ atuin_unzip_dir }}/atuin {{ atuin_install_path }}"
  when: atuin_unzip.changed

- name: Clean unzip dir
  file:
    path: "/tmp/{{ atuin_unzip_dir }}"
    state: absent

- name: Create config dir
  file:
    path: ~/.config/atuin
    state: directory

- name: Create config
  copy:
    src: config.toml
    dest: ~/.config/atuin/config.toml

- name: Create an atuin bin symlink
  become: true
  file:
    src: "{{ atuin_install_path }}"
    dest: /usr/local/bin/atuin
    state: link

My Atuin config.toml (referenced above) is rather stock.

auto_sync = true
update_check = false
sync_address = "https://atuin.example.com"
sync_frequency = "1m"
enter_accept = true

[sync]
records = true

[ui]
columns = ["time", { type= "host", width = 10 }, "command"]

Registering on First Host

The fact that Atuin needs a username, password, and an encryption key felt a bit more complicated than necessary, but you can still set up most of this with Ansible. First you need to register with the Atuin server on one host to set up a user profile and get an encryption key by following the instructions in their docs. Then you can grab the key by running atuin key on that host.

NOTE: Don’t grab the key from ~/.local/share/atuin/key like I first did. It is stored in a different format than what the atuin login command expects.

Login on other Hosts

Once you have an account set up, you can add the following to your Ansible playbook and have Ansible login on your other hosts.

- name: Set vars
  set_fact:
    atuin_username: "<YOUR-USER>"
    atuin_password: "<YOUR-PASSWORD>"
    atuin_key: "<YOUR-KEY>"
    
- name: Check if already logged in
  command: atuin status
  register: atuin_status
  failed_when: false
  changed_when: false

- name: Log into Atuin if status check failed
  shell: >
    atuin login
    -u {{ atuin_username }}
    -p {{ atuin_password }}
    -k "{{ atuin_key }}"
  when: atuin_status.rc > 0
  no_log: true  

NOTE: I’d suggest storing the atuin_password and atuin_key securely by reading the Ansible Docs. I’m just trying to keep the examples simple.

Activate in Your Shell

Then in my ~/.zshrc file we start up Atuin.

eval "$(atuin init zsh --disable-up-arrow)"

And happily back up, sync, and search our CLI history!

Demo