---
title: "Glazier Release: v0.2.4"
description: "Last night, I minted and tagged Glazier v0.2.4. This release focussed on addressing 80 bugs and stability issues I’ve collated over the last few months. I initially thought I’d take care of a dozen or so HIGH and MEDIUM ones, but ended up spending the better part of the last week working through all of them."
published_at: "2026-10-03 00:00 +0000"
published: false
tags: ["go","hcl","tmux","glazier"]
cover_image: https://wilhelm.codes/blog/glazier-release-024/og.png
canonical_url: https://wilhelm.codes/blog/glazier-release-024/
---

Last night, I minted and tagged [Glazier](https://github.com/wilhelm-murdoch/glazier) `v0.2.4`. This release focussed on addressing **80** bugs and stability issues I've collated over the last few months. I initially thought I'd take care of a dozen or so `HIGH` and `MEDIUM` ones, but ended up spending the better part of the last week working through _all_ of them.

## I might've gone a little overboard.

Yes, you read that right. _Dozens_ of bugs covering the full spectrum of compatibility, stability, security and just plain broken functionality. Here's the list in its entirety:

| Bug | Problem |
| --- | ------- |
| `B01` | tmux matched session names by prefix, so `glaze down` on a session named `project` could kill `project-long`. |
| `B02` | A window with four or more panes failed with "no space for new pane". |
| `B03` | Panes and windows ignored the `starting_directory` of their window and session. |
| `B04` | A non-final command ending in `&`, `;` or a comment hung `up`. |
| `B05` | The layout overwrote `size` and `adjust`, so neither had any effect. |
| `B06` | Session commands hung or failed with some session names. |
| `B07` | Logs and diagnostics went to stdout, so `save --stdout > file` wrote an invalid profile. |
| `B08` | A failed `glaze up` left a partial session behind a subsequent `glaze up` would accept. |
| `B09` | The first window index was `base-index + 1`. |
| `B10` | A semicolon or a tab in a name broke the parsing of tmux output. |
| `B11` | `save` overwrote an existing `.glaze` without asking. |
| `B12` | `save` outside tmux picked a session without a warning. |
| `B13` | Validation accepted some values that `up` could not apply. |
| `B14` | Diagnostics contained ANSI colour codes when the output was not a terminal. |
| `B15` | Diagnostics for a syntax error showed "(source code not available)". |
| `B16` | The README and `--help` claimed that `--var-file` reads JSON; it reads HCL only. |
| `B17` | The README example for the `session-created` hook could never fire. |
| `B18` | A `~` in `--profile-path` or `${GLAZE_PATH}` was not expanded. |
| `B19` | `ls` exited 1 when no tmux server was running. |
| `B20` | `attach` passed the socket flag twice. |
| `B21` | tmux expanded format sequences such as `#{session_name}` inside names. |
| `B22` | `save` wrote directories that no longer existed. |
| `B23` | The README did not say that `reverse` only accepts a string. |
| `B24` | tmux quietly changed `.` and `:` in a session name. |
| `B25` | With tmux 3.4, a `$` in a name gained an extra backslash on every read. |
| `B26` | `up --clear` from inside the target session killed its own shell and left nothing behind. |
| `B27` | `~/` in `starting_directory` passed validation but reached tmux unexpanded. |
| `B28` | `${TMUX}` was trusted without a socket check, so `up` could switch a client on another server. |
| `B29` | `format` and `save` truncated the file before writing, so a failed write destroyed the only copy. |
| `B30` | `--debug` printed every `envs` value, secrets included. |
| `B31` | `down` panicked when the session name evaluated to a number, a bool or null. |
| `B32` | A session without a `name` worked with `up`, but `down` rejected the same profile. |
| `B33` | `down` rolled `random()` in the session name again and killed the wrong session. |
| `B34` | `down`, `ls` and `save` reported "not running" when tmux was merely unreachable. |
| `B35` | tmux split an argument ending in `;`, which broke `up` with an odd error. |
| `B36` | A `!` in a command triggered history expansion in bash and zsh. Oops. |
| `B37` | Readline completion ate a tab in a command. |
| `B38` | The `wait-for` signal relied on `tmux` from `${PATH}` and `${TMUX}`, so a changed environment hung `up`. |
| `B39` | A pane whose command exits at once broke the next split. |
| `B40` | A session option declared on a window leaked to every later window. |
| `B41` | A window or pane option declared on the session landed on a throwaway window and was lost. |
| `B42` | A `base-index` set in the background by `tmux.conf` made glaze kill the first declared window. |
| `B43` | `-v` after a subcommand was ignored, so `glaze up -v` built a session. |
| `B44` | Under `LANG=C`, non-ASCII names read back as `_` and broke a second `up` and `save`. |
| `B45` | A small chain of `locals` could exhaust memory, even in `down` and `format --validate`. |
| `B46` | Deeply nested brackets (about 100,000 levels) crashed glaze with a stack overflow. |
| `B47` | A `size` with only `x` or only `y` passed validation but was never applied. |
| `B48` | A percentage `adjust.amount` passed validation, then failed halfway through `up`. |
| `B49` | `make install` on Apple Silicon built an amd64 binary outside `${PATH}`. |
| `B50` | A null element in a list or map panicked `up` and `format --validate`. |
| `B51` | A command starting with `-` was read as `send-keys` flags. |
| `B52` | A final command equal to a tmux key name, such as `Enter`, was sent as that key. |
| `B53` | The shell's line editor cut long command lines, which hung `up`. |
| `B54` | `SIGINT` or `SIGTERM` during a wait left an orphaned tmux client and a partial session. |
| `B55` | A bare `~` was rejected, and `~user` gave a misleading error. |
| `B56` | glaze failed from a deleted working directory, even with an absolute profile path. |
| `B57` | A `${GLAZE_PATH}` naming a file, or a `.glaze` directory, gave "profile not found" with no hint. |
| `B58` | `format` hung on a FIFO and dropped its output on `/dev/stdin`. |
| `B59` | `format` rewrote an unchanged file and bumped its modification time. |
| `B60` | `down` without a required `--var` reported "Unsupported attribute". |
| `B61` | A cycle between `locals` showed up as three misleading errors. |
| `B62` | `random()` on a map put a Go stack dump in its diagnostic. |
| `B63` | `save` of a session that glaze did not create named every pane after the host and every window after the shell. |
| `B64` | Two `up` runs at the same time left the loser with an opaque error. |
| `B65` | glaze read `base-index` over and over, so `ls` on 80 windows spawned 83 processes. |
| `B66` | A `tmux.conf` hook could rename every declared window in silence. |
| `B67` | A raw layout with the wrong number of panes was applied in part without a word. |
| `B68` | Unknown hook names passed validation and failed halfway through `up`. |
| `B69` | A `bool` variable accepted `1` and `0`, which the README never mentioned. |
| `B70` | A binary from `go install` reported `Version: dev`. |
| `B71` | `--help` showed the author as a Go struct. |
| `B72` | `save` warned "EXPERIMENTAL" on every run. |
| `B73` | `${NO_COLOR}` was ignored in a pipe. |
| `B74` | tmux 3.9 prints layouts as JSON, which `save` wrote and glaze then rejected. |
| `B75` | tmux read a window name starting with `-` as a flag, which failed `up`. |
| `B76` | `up` without a terminal exited 1 but left the session running. |
| `B77` | A failed write in `format` exited 3, the code for an invalid profile. |
| `B78` | `format --validate` printed its errors in a different order on each run. |
| `B79` | A signal during a wait was blamed on the pane's shell. |
| `B80` | Errors in a `--var-file` showed "(source code not available)". |

Some of these were just plain embarrassing, but I'm glad I was able to work through them all. Surely, there are many more lurking in the code somewhere, but each one is a learning opportunity. On the bright side, Glazier is now more stable than ever!

## We now have proper package management!

I also took the time to reduce the friction involved in actually installing the tool. I looked at something like [GoReleaser](https://goreleaser.com/), which would definitely have simplified distribution, but it would've come at the cost of the current release pipeline in GitHub. Definitely doable, but I'd have to bend the pipeline to fit the tool rather than the other way around. Besides, a handful of files was enough to get the following platforms sorted.

### Homebrew

This is probably the one that makes the most sense for a lot of fellow developers. The most tedious part was standing up an additional [repository](https://github.com/wilhelm-murdoch/homebrew-tap) to get a custom tap working. But now, installing Glazier on your Mac is as easy as:

```bash
brew install wilhelm-murdoch/tap/glazier
```

### APK, DEB and RPM packages

These were actually easier to implement, as each one is rendered by a simple [Bash](https://github.com/wilhelm-murdoch/glazier/blob/main/packaging/render.sh) script.

#### `.deb`

```bash
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.deb
sudo apt install ./glazier-linux-amd64.deb
```

#### `.rpm`

```bash
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.rpm
sudo dnf install ./glazier-linux-amd64.rpm
```

#### `.apk`

```bash
curl -LO https://github.com/wilhelm-murdoch/glazier/releases/latest/download/glazier-linux-amd64.apk
apk add --allow-untrusted ./glazier-linux-amd64.apk
```

> **Notice:** The `.apk` package isn't signed, so you'll have to use the `--allow-untrusted` flag for this to work. You can always verify it yourself against the release's attestation. Alternatively, if enough people ask, I'll go through the trouble of getting it properly signed.

### What about AUR support?

This was one of the last platforms I wanted to support. However, the AUR currently has a registration freeze in place due to a massive influx of requests. So, I'm putting this one on pause and I'll check back later.

## E2E testing gets a brand-new harness!

Most of these bugs were found by an end-to-end harness that drives the real binary against real tmux servers. It began life as a bunch'a Bash scripts. For this release, I rebuilt it as an extensible Go test module that lives in the repository under `e2e/`.

Every case starts its own tmux server in its own temporary directory, runs the `glaze` binary exactly as you would and reads the result back from tmux itself. The harness never imports glaze, so a bug cannot hide behind glaze's own code.

A few details:

- It currently has over `200` cases and more than `1,000` checks, with profiles kept as real `.glaze` fixture files.
- It covers six tmux versions from `3.2a` to `3.7c`, Alpine (musl) included, on `amd64` and `arm64`, plus macOS.
- Every pull request runs the full matrix and compares the result of each check against the `main` branch.
- A weekly canary runs against the development branch of tmux. It caught the JSON layouts of tmux 3.9 before any tmux release shipped them.
- Releases are now gated behind the harness, so a release tag publishes only when every case passes against it.
- Known failures live in a baseline that fails the run the moment a fix makes an entry stale.

The last big release, `v0.1.6`, failed 88 checks on _every_ target. At the time of this writing, all checks pass on all targets! This was arguably the biggest win for future releases because we now have near-full integration coverage on top of the 700+ unit tests... probably a bit excessive for this kind of tool. I'm nothing if not thorough.

The downside to this new harness is that it's one more thing I need to maintain alongside the rest of the code. We'll see how much of a headache that becomes.

## What's next?

Now that all _known_ bugs have been squashed, I can focus on some interesting new features. Specifically, something like `glaze diff ...` that compares a given session with the specified `.glaze` profile. Think of `terraform plan`, but for your tmux session. I'm also considering a registry, or pool, of _named_ profiles that can be called from anywhere regardless of directory context. I've also been thinking about lifecycle hooks outside of tmux, and about a way to _import_ your existing profiles from _other_ tools like `tmuxinator`, `tmuxp` and `smug`.

Then there's the repository's ever-growing `README.md`. It isn't exactly the most intuitive thing to peruse as a newcomer, and the `SPEC.md` file is a bit daunting. So, a dedicated documentation site is something I'm considering as a next step.

If there's one thing I've learned while building this tool it's that I _never_ want to build functionality _around_ an other well-known tool ever again. But, I probably will anyway. 

To the two other people using this, enjoy!
