All posts
4 min readDavid Nussio

Why there are no emoji in envsec's output

A CLI prints a tidy list with a padlock in front of every line. On your colleague's terminal the padlock eats the space after it, or the cursor ends up one column to the left of where you are typing. Nothing is broken in the program. The program and the terminal disagree about how wide one character is.

A terminal is a grid

A terminal draws text into a grid of cells. Every program that writes to it, and every line editor that moves the cursor around in it, has to know how many cells each character takes. The usual answer comes from wcwidth(3): 0 for combining marks, 1 for most characters, 2 for wide ones such as CJK ideographs.

The catch is that there is no single wcwidth. Your libc has one, which shells and line editors usually call. Terminal multiplexers and terminal emulators often ship their own tables. Each is built from some version of the Unicode data, and the versions don't always agree. Plain ASCII never trips over this. Emoji do, all the time.

Four ways an emoji breaks the grid

Here is what macOS's own libc reports for a few of them:

SequenceCode pointsEast_Asian_Widthwcswidth
๐Ÿ”‘ old key iconU+1F511W2
๐Ÿ›ก old shield iconU+1F6E1N1
๐Ÿ›ก๏ธ shield + VS16U+1F6E1 U+FE0FN (base)1
โš ๏ธ warning + VS16U+26A0 U+FE0FN (base)1
๐Ÿ‘ฉโ€๐Ÿ’ป ZWJ sequenceU+1F469 U+200D U+1F4BBW (base)4
โ—† current key iconU+25C6A1
wcswidth from macOS libc (Darwin 27, en_US.UTF-8 locale). East_Asian_Width from Python's unicodedata (Unicode 16.0).

The 1s next to the VS16 sequences and the 4 next to the ZWJ sequence are where libc and a terminal that draws colour emoji are likely to disagree. When one line in a list is off by a cell, the columns after it no longer line up, and a line editor that guessed wrong puts the cursor in the wrong place.

What I changed in envsec

envsec's first icon set had ๐Ÿ”‘ for keys, ๐Ÿ”’ for locks, ๐Ÿ“ for contexts, ๐Ÿ›ก for the doctor and โœ… for success. In March 2026 I replaced every emoji in the set with a geometric Unicode glyph in one commit (6709620da). An excerpt of the diff:

diff
-  key: yellow("๐Ÿ”‘"), // U+1F511
-  lock: green("๐Ÿ”’"), // U+1F512
-  folder: blue("๐Ÿ“"), // U+1F4C1
-  shield: green("๐Ÿ›ก"), // U+1F6E1
-  check: green("โœ…"), // U+2705
-  broom: yellow("๐Ÿงน"), // U+1F9F9
+  key: yellow("โ—†"), // U+25C6
+  lock: green("โ– "), // U+25A0
+  folder: blue("โ–ธ"), // U+25B8
+  shield: green("โ—ˆ"), // U+25C8
+  check: green("โœ”"), // U+2714
+  broom: yellow("~"), // tilde

The same commit added a rule to the agent instructions in the repo: all icons live in one icons object (today in packages/core/src/ui.ts), every glyph carries a comment with its code point, each is wrapped in its colour function, and emoji are not allowed. Here is the complete set:

IconGlyphCode pointColourEAWMeaning
arrowโ†’U+2192dimAfrom โ†’ to
boltโ€บU+203AyellowNsaved command
broom~U+007EyellowNastale records removed
cancelโŠ˜U+2298dimNcancelled
chartโ–ชU+25AAblueNsummary line
checkโœ”U+2714greenNall clear
clockโ—”U+25D4yellowNexpires soon
diceโฌกU+2B21magentaNgenerated secret
downloadโ†“U+2193cyanATUI only
emptyโˆ…U+2205dimNnothing found
env$U+0024cyanNanot used yet
errorโœ–U+2716redNerror
expiredโœ–U+2716redNexpired
fileยทU+00B7cyanA.env file
folderโ–ธU+25B8blueNcontext
infoโ—U+25CFblueAinformation
keyโ—†U+25C6yellowAsecret key
lockโ– U+25A0greenAresolved, secured
saveโ†“U+2193greenAcommand saved
searchโ—ŽU+25CEblueAsearch
shellโ–ถU+25B6greenAsubshell, next step
shieldโ—ˆU+25C8greenAdoctor, encryption
successโœ”U+2714greenNdone
trashร—U+00D7redAremoved
unlockโ–กU+25A1redAnot used yet
uploadโ†‘U+2191magentaATUI only
warningโ–ฒU+25B2yellowAwarning, confirm
All 27 entries of the icons object in packages/core/src/ui.ts. EAW = East_Asian_Width. Meaning is where the CLI uses each icon.

None of these has the Emoji_Presentation property, and envsec never emits U+FE0F. The source tree has no variation selectors at all. Secret values can still contain emoji; envsec only declines to add its own.

Not a perfect set

I'd like to say these glyphs are always one cell. They aren't. 13 of the 24 distinct glyphs, including โ†’ โ— โ—† โ–  โ–ฒ and ร—, are East Asian Ambiguous. Ambiguous characters are one cell in most Western setups and two cells in many CJK ones, and several terminals (and Vim, via ambiwidth) let you choose. Four of them, โœ” โœ– โ–ช and โ–ถ, still carry the Emoji property: their default is text, but a renderer that ignores presentation defaults could still pick an emoji font.

The trade is between failure modes. An emoji depends on the Unicode version, the font, the selector and the terminal, and can be wrong in a different way on every line. An ambiguous-width glyph depends on one setting, and when it is wrong, every line is wrong by the same amount.

Colour, NO_COLOR and pipes

Colour is plain ANSI SGR escapes, decided once per output stream when ui.ts loads:

ts
export const isColorEnabled = (
  stream: { readonly isTTY?: boolean },
  env: NodeJS.ProcessEnv = process.env
): boolean => {
  if (env.NO_COLOR) {
    return false;
  }
  const force = env.FORCE_COLOR;
  if (force !== undefined) {
    return force !== "0" && force !== "false";
  }
  return stream.isTTY ?? false;
};

const createUi = (useColor: boolean) => {
  const ansi = (code: string) => (text: string) =>
    useColor ? `\u001B[${code}m${text}\u001B[0m` : text;
  // ... colours, icons and helpers built from ansi()
};

// stdout for command output, stderr for warnings and errors
export const { green, red, icons /* ... */ } = createUi(
  isColorEnabled(process.stdout)
);
export const stderrUi = createUi(isColorEnabled(process.stderr));

That gives three rules, in this order:

Piping drops the escapes but keeps the glyphs, so the output of envsec list | cat is still readable:

text
โ–ธ myapp.dev  (3 secrets)
โ–ธ myapp.prod  (2 secrets)

Two rough edges I found while checking this, both fixed in envsec 1.1.3. FORCE_COLOR=0 used to force colour on, because the check only tested for a non-empty string, while libraries such as supports-color treat 0 as off. And there was one TTY test, on stdout, even for errors written to stderr: run envsec in a terminal with 2> err.log and the log file got escape codes, while envsec env | โ€ฆ printed its warnings to the terminal without colour. Each stream now gets its own palette. Scripts that want clean bytes can still set NO_COLOR=1 or use --json where a command supports it.

In short

Emoji width depends on the Unicode version, the font, variation selectors and joiners, and the terminal, and the program printing them controls none of those. envsec uses 24 geometric glyphs with no emoji presentation, wrapped in ANSI colours that switch off with NO_COLOR or a pipe. They still have one known weak spot, ambiguous width, but they fail the same way on every line. For the other side of small CLI details, see how envsec completes contexts and keys in bash, zsh and fish.