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
- Width tables. Unicode's East_Asian_Width property (UAX #11) classifies characters as Wide (W), Narrow (Na), Neutral (N) or Ambiguous (A), among others. Since Unicode 9.0, emoji that display as emoji by default are W. A component that still uses an older table counts them as 1 cell while the terminal draws 2.
- Variation selectors. Some characters, like ๐ก (U+1F6E1) and โ (U+26A0), have text presentation by default. Add U+FE0F (VARIATION SELECTOR-16) and they become emoji. The selector itself has width 0, so
wcswidthstill says 1, while many terminals paint a two-cell colour glyph. - ZWJ sequences. ๐ฉโ๐ป is three code points joined by a ZERO WIDTH JOINER. Summing per-code-point widths gives 4. A terminal that knows the sequence draws one 2-cell glyph; one that doesn't draws two.
- Fonts. Monospace programming fonts rarely include emoji, so the terminal falls back to a colour emoji font whose glyph doesn't fit the cell. Some terminals scale it down, some let it overflow into the next cell.
Here is what macOS's own libc reports for a few of them:
| Sequence | Code points | East_Asian_Width | wcswidth |
|---|---|---|---|
| ๐ old key icon | U+1F511 | W | 2 |
| ๐ก old shield icon | U+1F6E1 | N | 1 |
| ๐ก๏ธ shield + VS16 | U+1F6E1 U+FE0F | N (base) | 1 |
| โ ๏ธ warning + VS16 | U+26A0 U+FE0F | N (base) | 1 |
| ๐ฉโ๐ป ZWJ sequence | U+1F469 U+200D U+1F4BB | W (base) | 4 |
| โ current key icon | U+25C6 | A | 1 |
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:
- 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("~"), // tildeThe 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:
| Icon | Glyph | Code point | Colour | EAW | Meaning |
|---|---|---|---|---|---|
| arrow | โ | U+2192 | dim | A | from โ to |
| bolt | โบ | U+203A | yellow | N | saved command |
| broom | ~ | U+007E | yellow | Na | stale records removed |
| cancel | โ | U+2298 | dim | N | cancelled |
| chart | โช | U+25AA | blue | N | summary line |
| check | โ | U+2714 | green | N | all clear |
| clock | โ | U+25D4 | yellow | N | expires soon |
| dice | โฌก | U+2B21 | magenta | N | generated secret |
| download | โ | U+2193 | cyan | A | TUI only |
| empty | โ | U+2205 | dim | N | nothing found |
| env | $ | U+0024 | cyan | Na | not used yet |
| error | โ | U+2716 | red | N | error |
| expired | โ | U+2716 | red | N | expired |
| file | ยท | U+00B7 | cyan | A | .env file |
| folder | โธ | U+25B8 | blue | N | context |
| info | โ | U+25CF | blue | A | information |
| key | โ | U+25C6 | yellow | A | secret key |
| lock | โ | U+25A0 | green | A | resolved, secured |
| save | โ | U+2193 | green | A | command saved |
| search | โ | U+25CE | blue | A | search |
| shell | โถ | U+25B6 | green | A | subshell, next step |
| shield | โ | U+25C8 | green | A | doctor, encryption |
| success | โ | U+2714 | green | N | done |
| trash | ร | U+00D7 | red | A | removed |
| unlock | โก | U+25A1 | red | A | not used yet |
| upload | โ | U+2191 | magenta | A | TUI only |
| warning | โฒ | U+25B2 | yellow | A | warning, confirm |
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:
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:
NO_COLORset to any non-empty value turns colour off, as the NO_COLOR convention asks. An emptyNO_COLOR=is ignored, which also matches it.FORCE_COLORturns it on, unless it is0orfalse, which turn it off. That is how Node.js itself reads the variable.- Otherwise colour is on only when the stream being written is a TTY: stdout for command output, stderr for warnings and errors.
Piping drops the escapes but keeps the glyphs, so the output of envsec list | cat is still readable:
โธ 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.