<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>FLAMEEE</title><description>Offensive security notes by Pedro Augusto de Oliveira (flameee): pentest, red team and AI/LLM security, plus CTF and bug bounty writeups.</description><link>https://flameee-sec.netlify.app/</link><language>en</language><item><title>The edge reads you before the payload</title><link>https://flameee-sec.netlify.app/posts/tls-fingerprint-what-the-edge-sees/</link><guid isPermaLink="true">https://flameee-sec.netlify.app/posts/tls-fingerprint-what-the-edge-sees/</guid><description>How a server decides you are a tool before reading a single byte of your request: the ClientHello field by field, the JA3 that Chrome deliberately broke in 2023, the JA4 that replaced it, and the other layers that count too. With measurements from this machine.</description><pubDate>Tue, 03 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The scene repeats itself. The request works in the browser and fails in &lt;code&gt;curl&lt;/code&gt;. You check the header, the token, the body, all identical, and conclude the target “detects tooling”. So you improve the User-Agent, add Chrome’s headers, and keep collecting 403s.&lt;/p&gt;
&lt;p&gt;What took me a long time to internalise is that, at that point, &lt;strong&gt;the server has probably not read anything you sent&lt;/strong&gt;. The decision was already made in the handshake, based on how your client &lt;em&gt;introduced itself&lt;/em&gt; — before HTTP existed, before a header existed, before a payload existed. There is a whole layer of identity that I spent years filing under “networking detail”, and that in practice decides whether my test starts at all.&lt;/p&gt;
&lt;p&gt;This post is about that layer. I am going to open the ClientHello field by field, show why every TLS library produces a stable signature, explain JA3 and why it died for browsers in 2023, decode JA4 end to end, and finish with the layers that are not TLS but count just the same. All of it measured, none of it “they say”.&lt;/p&gt;
&lt;aside class=&quot;markdown-alert markdown-alert-note&quot; dir=&quot;auto&quot;&gt;
&lt;p class=&quot;markdown-alert-title&quot; dir=&quot;auto&quot;&gt;&lt;svg class=&quot;octicon&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;NOTE&lt;/p&gt;
&lt;p&gt;The measurements come from a local endpoint that reads the raw ClientHello and computes JA3/JA4, fed by 10 different stacks (38 connections). Where I quote a fingerprint, it came from a record. Where I describe behaviour, I name the exact version. In this subject, version is half the answer.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2 id=&quot;anatomy-of-a-clienthello&quot;&gt;Anatomy of a ClientHello&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#anatomy-of-a-clienthello&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before any fingerprint, the object. The current standard is &lt;strong&gt;RFC 9846&lt;/strong&gt; (July 2026), which obsoletes RFC 8446. Yes, TLS 1.3 was republished, and almost everything you will read still cites 8446. The ClientHello structure is unchanged:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;struct {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ProtocolVersion legacy_version = 0x0303;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Random random;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    opaque legacy_session_id&amp;#x3C;0..32&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    CipherSuite cipher_suites&amp;#x3C;2..2^16-2&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    opaque legacy_compression_methods&amp;#x3C;1..2^8-1&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Extension extensions&amp;#x3C;8..2^16-1&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;} ClientHello;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Six fields. I will walk through each one with a single question in mind: &lt;strong&gt;is this signal, or is it noise?&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Field&quot;&gt;














































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Field&lt;/th&gt;&lt;th&gt;Bytes&lt;/th&gt;&lt;th&gt;What it is&lt;/th&gt;&lt;th&gt;Does it fingerprint?&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;legacy_version&lt;/code&gt;&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;Pinned to &lt;code&gt;0x0303&lt;/code&gt; in TLS 1.3&lt;/td&gt;&lt;td&gt;Barely: everyone sends the same thing&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;random&lt;/code&gt;&lt;/td&gt;&lt;td&gt;32&lt;/td&gt;&lt;td&gt;Drawn fresh on every connection&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Never.&lt;/strong&gt; If it shows up in a hash, the hash is broken&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;legacy_session_id&lt;/code&gt;&lt;/td&gt;&lt;td&gt;0–32&lt;/td&gt;&lt;td&gt;In compatibility mode, 32 random bytes&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;: presence and length separate clients&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;cipher_suites&lt;/code&gt;&lt;/td&gt;&lt;td&gt;variable&lt;/td&gt;&lt;td&gt;Suite list, in preference order&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Heavily&lt;/strong&gt;: the set &lt;em&gt;and&lt;/em&gt; the order&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;legacy_compression_methods&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;Exactly &lt;code&gt;00&lt;/code&gt; in TLS 1.3&lt;/td&gt;&lt;td&gt;A constant today&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;extensions&lt;/code&gt;&lt;/td&gt;&lt;td&gt;variable&lt;/td&gt;&lt;td&gt;Set, order and content&lt;/td&gt;&lt;td&gt;&lt;strong&gt;The richest field of all&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Three things here deserve attention, because they are where the mental model usually breaks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;legacy_version&lt;/code&gt; is a lie by design.&lt;/strong&gt; TLS 1.3 does not state its version there; it says &lt;code&gt;0x0303&lt;/code&gt; (TLS 1.2) and puts the real version in an extension, &lt;code&gt;supported_versions&lt;/code&gt;. The reason is historical: old middleboxes rejected hellos carrying a higher version number, so the legacy field was frozen to slip past dumb equipment. The direct consequence for fingerprinting: &lt;strong&gt;JA3 reads the legacy field, so it cannot tell 1.2 from 1.3&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The 32-byte session id is “compatibility mode”&lt;/strong&gt; (Appendix E.4 of RFC 9846). To make a 1.3 handshake look like a 1.2 resumption, the client sends a non-empty session id plus a dummy &lt;code&gt;change_cipher_spec&lt;/code&gt;. In my measurements, &lt;strong&gt;every TLS 1.3 connection from all 10 stacks sent 32 bytes&lt;/strong&gt;, and every client restricted to TLS 1.2 sent an empty one, with one exception (Go, which sends 32 bytes in both cases). So an empty session id in a 1.3 hello is a strong “not a browser” signal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;random&lt;/code&gt; does not count.&lt;/strong&gt; Across two Python &lt;code&gt;requests&lt;/code&gt; runs the raw ClientHello bytes differ (&lt;code&gt;0adf02f5c7e64d31…&lt;/code&gt; versus &lt;code&gt;60bebb560e3f784a…&lt;/code&gt;) while JA3 and JA4 are identical. That is the definition of a fingerprint that works: the bytes change, the structure does not.&lt;/p&gt;
&lt;h2 id=&quot;the-extensions-are-the-signature&quot;&gt;The extensions are the signature&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#the-extensions-are-the-signature&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you memorise one table in this post, make it this one. Extensions are what separates one client from another, and each is identified by a 16-bit number. These are the codepoints that actually show up, checked against the IANA registry:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: hex&quot;&gt;
















































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;hex&lt;/th&gt;&lt;th&gt;Name&lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;th&gt;Where it lands&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x0000&lt;/code&gt;&lt;/td&gt;&lt;td&gt;server_name&lt;/td&gt;&lt;td&gt;SNI: the host you want&lt;/td&gt;&lt;td&gt;&lt;code&gt;d&lt;/code&gt;/&lt;code&gt;i&lt;/code&gt; in JA4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x0005&lt;/code&gt;&lt;/td&gt;&lt;td&gt;status_request&lt;/td&gt;&lt;td&gt;asks for OCSP stapling&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x000a&lt;/code&gt;&lt;/td&gt;&lt;td&gt;supported_groups&lt;/td&gt;&lt;td&gt;curves/groups accepted&lt;/td&gt;&lt;td&gt;JA3 (field 4), &lt;strong&gt;absent&lt;/strong&gt; from JA4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x000b&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ec_point_formats&lt;/td&gt;&lt;td&gt;EC point format&lt;/td&gt;&lt;td&gt;JA3 (field 5)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x000d&lt;/code&gt;&lt;/td&gt;&lt;td&gt;signature_algorithms&lt;/td&gt;&lt;td&gt;signature algorithms&lt;/td&gt;&lt;td&gt;JA4, &lt;strong&gt;in order&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x0010&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ALPN&lt;/td&gt;&lt;td&gt;&lt;code&gt;h2&lt;/code&gt;, &lt;code&gt;http/1.1&lt;/code&gt;…&lt;/td&gt;&lt;td&gt;&lt;code&gt;h2&lt;/code&gt;/&lt;code&gt;h1&lt;/code&gt; in JA4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x0012&lt;/code&gt;&lt;/td&gt;&lt;td&gt;signed_certificate_timestamp&lt;/td&gt;&lt;td&gt;SCT (CT)&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x0015&lt;/code&gt;&lt;/td&gt;&lt;td&gt;padding&lt;/td&gt;&lt;td&gt;hello padding&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x0017&lt;/code&gt;&lt;/td&gt;&lt;td&gt;extended_master_secret&lt;/td&gt;&lt;td&gt;1.2 legacy&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x001b&lt;/code&gt;&lt;/td&gt;&lt;td&gt;compress_certificate&lt;/td&gt;&lt;td&gt;certificate compression&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x0023&lt;/code&gt;&lt;/td&gt;&lt;td&gt;session_ticket&lt;/td&gt;&lt;td&gt;resumption&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x002b&lt;/code&gt;&lt;/td&gt;&lt;td&gt;supported_versions&lt;/td&gt;&lt;td&gt;&lt;strong&gt;the real version&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;13&lt;/code&gt;/&lt;code&gt;12&lt;/code&gt; in JA4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x002d&lt;/code&gt;&lt;/td&gt;&lt;td&gt;psk_key_exchange_modes&lt;/td&gt;&lt;td&gt;PSK modes&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x0033&lt;/code&gt;&lt;/td&gt;&lt;td&gt;key_share&lt;/td&gt;&lt;td&gt;public key exchange material&lt;/td&gt;&lt;td&gt;count, &lt;strong&gt;content never&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0xff01&lt;/code&gt;&lt;/td&gt;&lt;td&gt;renegotiation_info&lt;/td&gt;&lt;td&gt;anti-renegotiation&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0xfe0d&lt;/code&gt;&lt;/td&gt;&lt;td&gt;encrypted_client_hello&lt;/td&gt;&lt;td&gt;ECH&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;0x44cd&lt;/code&gt;&lt;/td&gt;&lt;td&gt;application_settings (ALPS)&lt;/td&gt;&lt;td&gt;application settings&lt;/td&gt;&lt;td&gt;set&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Two rows in that table deserve their own paragraph, because they are the ones I have seen described wrongly most often.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;0xfe0d&lt;/code&gt; in Chrome is ECH &lt;em&gt;GREASE&lt;/em&gt;, not real ECH.&lt;/strong&gt; Chrome calls &lt;code&gt;SSL_set_enable_ech_grease(ssl, 1)&lt;/code&gt; on the opportunistic path, and BoringSSL documents that this “sends a GREASE ECH extension when no supported ECHConfig is available”. Since &lt;code&gt;localhost&lt;/code&gt; has no ECHConfig in DNS, what goes out is a fake ECH: 218 bytes on the connection I inspected, with a random payload and a size that varies. The extension exists, but it is hiding nothing: it is extensibility noise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;0x44cd&lt;/code&gt; is real ALPS, and it is unregistered.&lt;/strong&gt; The payload I captured is &lt;code&gt;00 03 02 68 32&lt;/code&gt;: a list length, a protocol length, and the letters &lt;code&gt;h2&lt;/code&gt;. It literally says “my application settings are for h2”. Cross-check: on Chrome running with &lt;code&gt;--disable-http2&lt;/code&gt; that extension &lt;strong&gt;disappears&lt;/strong&gt;, and the hello drops from 17 to 16 extensions. BoringSSL calls this family “ALPS (draft-vvv-tls-alps)” and keeps two codepoints (old and new, selected by &lt;code&gt;alps_use_new_codepoint&lt;/code&gt;), while the draft leaves the IANA value as &lt;code&gt;TBD&lt;/code&gt;. That is why the number you see on the wire is experimental and missing from the registry.&lt;/p&gt;
&lt;h2 id=&quot;grease-noise-on-purpose&quot;&gt;GREASE: noise on purpose&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#grease-noise-on-purpose&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;RFC 8701 reserves values to be advertised at random: &lt;code&gt;0x0A0A&lt;/code&gt; through &lt;code&gt;0xFAFA&lt;/code&gt; for extensions, groups, signature algorithms and versions; &lt;code&gt;{0x0A,0x0A}&lt;/code&gt; through &lt;code&gt;{0xFA,0xFA}&lt;/code&gt; for cipher suites and ALPN. The point is to force the other end to ignore what it does not know, so the ecosystem does not seize up when a new value appears.&lt;/p&gt;
&lt;p&gt;For fingerprinting, GREASE is noise injected on purpose, and an accidental anti-fingerprint: the client changes its hello without changing software. In the measurements, &lt;strong&gt;only Chrome (and &lt;code&gt;curl_cffi&lt;/code&gt;, which copies Chrome) sends GREASE&lt;/strong&gt;: one cipher, two extensions, one group, one &lt;code&gt;supported_versions&lt;/code&gt; entry and one signature algorithm, with values re-randomised on every connection. The other nine stacks send none.&lt;/p&gt;
&lt;aside class=&quot;markdown-alert markdown-alert-important&quot; dir=&quot;auto&quot;&gt;
&lt;p class=&quot;markdown-alert-title&quot; dir=&quot;auto&quot;&gt;&lt;svg class=&quot;octicon&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;IMPORTANT&lt;/p&gt;
&lt;p&gt;“JA3 doesn’t handle GREASE” is a sentence that gets repeated a lot. It is false for the reference implementation: Salesforce’s &lt;code&gt;ja3.py&lt;/code&gt; carries a &lt;code&gt;GREASE_TABLE&lt;/code&gt; and filters ciphers, extensions and curves; the Zeek script does the same. The real problem is different: &lt;strong&gt;third-party implementations disagree with each other&lt;/strong&gt;. Fastly documented this with measurements: the ja3er service dropped the ALPS extension and returned an invalid hash, and in a second test a single curve (x448) appeared as &lt;code&gt;30&lt;/code&gt; on one side and &lt;code&gt;1035&lt;/code&gt; on the other. The defect is in the implementation, not the algorithm.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2 id=&quot;tls-12-and-13-dont-look-alike-on-the-wire&quot;&gt;TLS 1.2 and 1.3 don’t look alike on the wire&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#tls-12-and-13-dont-look-alike-on-the-wire&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Worth putting side by side, because it is the easiest difference to lose:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: TLS 1.2&quot;&gt;


































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;TLS 1.2&lt;/th&gt;&lt;th&gt;TLS 1.3&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;version&lt;/td&gt;&lt;td&gt;&lt;code&gt;legacy_version&lt;/code&gt; and nothing else&lt;/td&gt;&lt;td&gt;fixed &lt;code&gt;legacy_version&lt;/code&gt; + &lt;code&gt;supported_versions&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;key exchange&lt;/td&gt;&lt;td&gt;groups and point formats&lt;/td&gt;&lt;td&gt;&lt;code&gt;key_share&lt;/code&gt; with material in the hello&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;compression&lt;/td&gt;&lt;td&gt;a real list&lt;/td&gt;&lt;td&gt;exactly &lt;code&gt;00&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;session id&lt;/td&gt;&lt;td&gt;real resumption&lt;/td&gt;&lt;td&gt;32 random bytes (compat)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;resumption&lt;/td&gt;&lt;td&gt;session id / ticket&lt;/td&gt;&lt;td&gt;PSK + &lt;code&gt;psk_key_exchange_modes&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The effect on fingerprints is direct and a little cruel: because JA3 reads the legacy version, &lt;strong&gt;its version field is the same&lt;/strong&gt; (&lt;code&gt;771&lt;/code&gt;) for 1.2 and for 1.3. What separates the two hellos is the rest of the list, not the version. JA4 reads &lt;code&gt;supported_versions&lt;/code&gt; and marks &lt;code&gt;t12&lt;/code&gt; against &lt;code&gt;t13&lt;/code&gt; in the first part. And since 1.3 requires &lt;code&gt;key_share&lt;/code&gt; and &lt;code&gt;psk_key_exchange_modes&lt;/code&gt;, the extension count moves with it.&lt;/p&gt;
&lt;h2 id=&quot;every-stack-writes-its-own-hello&quot;&gt;Every stack writes its own hello&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#every-stack-writes-its-own-hello&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here is the structural insight that makes the rest of this make sense: &lt;strong&gt;the ClientHello is not negotiated, it is written&lt;/strong&gt;. It is the output of a table compiled into the binary.&lt;/p&gt;
&lt;p&gt;In BoringSSL, &lt;code&gt;ssl/extensions.cc&lt;/code&gt; holds a &lt;code&gt;kExtensions[]&lt;/code&gt; table, and the builder walks that table calling each entry’s &lt;code&gt;add_clienthello&lt;/code&gt;. In practice the extension order is a compile-time constant, except when it is shuffled. We will get there. The cipher list is a table too: &lt;code&gt;kCiphers[]&lt;/code&gt;, in &lt;code&gt;ssl_cipher.cc&lt;/code&gt;, with the three TLS 1.3 suites in a fixed order.&lt;/p&gt;
&lt;p&gt;Except “compiled table” does not mean “identical across stacks”. Measuring the same request through different tools:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Client measured&quot;&gt;












































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Client measured&lt;/th&gt;&lt;th&gt;Ciphers / extensions (no GREASE)&lt;/th&gt;&lt;th&gt;JA4&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;macOS curl 8.7.1 (LibreSSL 3.3.6)&lt;/td&gt;&lt;td&gt;49 / 7&lt;/td&gt;&lt;td&gt;&lt;code&gt;t13d4907h2_0d8feac7bc37_7395dae3b2f3&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Homebrew curl 8.21.0 (OpenSSL 3.6.4)&lt;/td&gt;&lt;td&gt;30 / 12&lt;/td&gt;&lt;td&gt;&lt;code&gt;t13d3012h2_1d37bd780c83_882d495ac381&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Python 3.15 (requests and httpx, OpenSSL)&lt;/td&gt;&lt;td&gt;17 / 12&lt;/td&gt;&lt;td&gt;&lt;code&gt;t13d1712h1_ab0a1bf427ad_0d46a1bf4a7c&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Go 1.25 &lt;code&gt;net/http&lt;/code&gt;&lt;/td&gt;&lt;td&gt;13 / 12&lt;/td&gt;&lt;td&gt;&lt;code&gt;t13d1312h2_f57a46bbacb6_ab7e3b40a677&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Java 21.0.8 &lt;code&gt;java.net.http&lt;/code&gt;&lt;/td&gt;&lt;td&gt;37 / 13&lt;/td&gt;&lt;td&gt;&lt;code&gt;t13d3713h2_db35923f8641_7c76daad20ec&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Node 26.8.2 (undici)&lt;/td&gt;&lt;td&gt;52 / 12&lt;/td&gt;&lt;td&gt;&lt;code&gt;t13d5212h1_b262b3658495_8e6e362c5eac&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Chrome 156&lt;/td&gt;&lt;td&gt;15 / 17&lt;/td&gt;&lt;td&gt;&lt;code&gt;t13d1517h2_8daaf6152771_cb7bf5808d99&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Look at the first two rows. &lt;strong&gt;They are the two &lt;code&gt;curl&lt;/code&gt; on the same machine&lt;/strong&gt;: one is the system build, the other is Homebrew’s. Same command, same name, different fingerprint, because the macOS one uses LibreSSL and the Homebrew one uses OpenSSL. If you copied a Linux tutorial and ran it on a Mac, you are advertising a different stack.&lt;/p&gt;
&lt;p&gt;Language is not the signal either. Python and Node both sit on OpenSSL; Python sends 17 ciphers and Node sends 52. What counts is the library and its version, not the language.&lt;/p&gt;
&lt;aside class=&quot;markdown-alert markdown-alert-note&quot; dir=&quot;auto&quot;&gt;
&lt;p class=&quot;markdown-alert-title&quot; dir=&quot;auto&quot;&gt;&lt;svg class=&quot;octicon&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;NOTE&lt;/p&gt;
&lt;p&gt;The Chrome in these measurements ran headless through Playwright, on build 156.0.8078.80. Headless uses the same BoringSSL, but I did not compare it against a normal window. So read the Chrome row as “this build”, not as “Chrome”.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2 id=&quot;ja3-one-line-one-md5-and-a-problem&quot;&gt;JA3: one line, one MD5, and a problem&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#ja3-one-line-one-md5-and-a-problem&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;JA3 (Salesforce, 2017) is about as simple as an idea gets: take five fields from the hello, join them into text, hash it.&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Decimal values, &lt;code&gt;,&lt;/code&gt; between fields, &lt;code&gt;-&lt;/code&gt; inside a field, MD5 at the end. The example from the official README, which I recomputed:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  → ada70206e40642a3e4461f35503241d5&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It worked in 2017 because it was cheap to compute and discriminating enough. And it still works today for what it is: a way of saying “these bytes appeared in this order”.&lt;/p&gt;
&lt;p&gt;Except “in this order” is the fragile part, and the list of weaknesses is short and honest: order matters; there is no extension content (only the numbers); 1.2 and 1.3 blur together; MD5 is an identifier, not an authenticator; and the implementation divergence Fastly measured. None of those is fatal on its own. Together, they left JA3 ready to be killed.&lt;/p&gt;
&lt;p&gt;And in 2023 someone killed it on purpose.&lt;/p&gt;
&lt;h2 id=&quot;2023-chrome-broke-ja3-on-purpose&quot;&gt;2023: Chrome broke JA3 on purpose&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#2023-chrome-broke-ja3-on-purpose&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the Chromium source, &lt;code&gt;net/socket/ssl_client_socket_impl.cc&lt;/code&gt;, there is an unconditional line on the TLS connection path:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;CPP&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;cpp&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;SSL_set_permute_extensions&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;ssl_&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;(),&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On the other side, in BoringSSL, &lt;code&gt;ssl_setup_extension_permutation()&lt;/code&gt; runs a Fisher-Yates shuffle with &lt;code&gt;RAND_bytes&lt;/code&gt; over the &lt;code&gt;kExtensions&lt;/code&gt; indices — and the emit loop walks the permutation instead of the natural order. In plain terms: &lt;strong&gt;Chrome shuffles the extension order of the ClientHello on every connection&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The motive is anti-ossification, and it is a good motive: if servers get used to a fixed order, any future change to Chrome breaks the internet. Shuffling forces the ecosystem to treat order as irrelevant. The side effect is that JA3, which depends on exactly that order, turned into noise.&lt;/p&gt;
&lt;p&gt;Fastly measured the damage at the time: from 20 January 2023 the most common Chrome JA3 on their network fell away, and since the number of permutations is “nearly 15 factorial (about 10¹²)”, in practice every connection produces a unique JA3. Cloudflare put it bluntly: it made “older TLS fingerprints almost useless for identifying the latest version of Chrome”.&lt;/p&gt;
&lt;p&gt;Now the number I measured, because this is where theory becomes obvious. &lt;strong&gt;Three fresh connections from the same Chrome 156 build&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;connection 1  →  JA3 0e250605ce1f2c793bb1a2269dd30e01&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;connection 2  →  JA3 47e376a38884e22f8a6bb4661daa71a9&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;connection 3  →  JA3 5c34099f1b500a60c0deacb3f8d31187&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         all  →  JA4 t13d1517h2_8daaf6152771_cb7bf5808d99&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Three different JA3. One JA4. If you built a WAF rule on browser JA3, it has been dead for three years.&lt;/p&gt;
&lt;aside class=&quot;markdown-alert markdown-alert-warning&quot; dir=&quot;auto&quot;&gt;
&lt;p class=&quot;markdown-alert-title&quot; dir=&quot;auto&quot;&gt;&lt;svg class=&quot;octicon&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;WARNING&lt;/p&gt;
&lt;p&gt;And the effect does not stop at Chrome: &lt;code&gt;curl_cffi&lt;/code&gt; with &lt;code&gt;impersonate=chrome&lt;/code&gt; &lt;strong&gt;also permutes&lt;/strong&gt;, because fidelity to Chrome includes permuting. One run gave &lt;code&gt;892aa6c8…&lt;/code&gt;, the other &lt;code&gt;be08a432…&lt;/code&gt;, both with the same JA4 as Chrome. So if you judge “is my impersonator working” by looking at JA3, you will conclude it is broken.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2 id=&quot;ja4-how-to-read-the-whole-fingerprint&quot;&gt;JA4: how to read the whole fingerprint&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#ja4-how-to-read-the-whole-fingerprint&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;JA4 (FoxIO, 2023) solves this with two decisions: sort what is arbitrary, and put readable information in front of the hash. The format is &lt;code&gt;a_b_c&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;t13d1516h2_8daaf6152771_e5627efa2ab1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I am going to decode that specification example end to end, because it is the best study material there is, and because I recomputed every hash.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Part&quot;&gt;

















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Part&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;t&lt;/code&gt;&lt;/td&gt;&lt;td&gt;t&lt;/td&gt;&lt;td&gt;TLS over TCP (&lt;code&gt;q&lt;/code&gt; = QUIC, &lt;code&gt;d&lt;/code&gt; = DTLS)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;13&lt;/code&gt;&lt;/td&gt;&lt;td&gt;13&lt;/td&gt;&lt;td&gt;TLS 1.3, read from &lt;code&gt;supported_versions&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;d&lt;/code&gt;&lt;/td&gt;&lt;td&gt;d&lt;/td&gt;&lt;td&gt;SNI present (&lt;code&gt;i&lt;/code&gt; = no SNI, went to an IP)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;15&lt;/code&gt;&lt;/td&gt;&lt;td&gt;15&lt;/td&gt;&lt;td&gt;15 cipher suites, not counting GREASE&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;16&lt;/code&gt;&lt;/td&gt;&lt;td&gt;16&lt;/td&gt;&lt;td&gt;16 extensions, not counting GREASE&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;h2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;h2&lt;/td&gt;&lt;td&gt;first and last character of the first ALPN&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;8daaf6152771&lt;/code&gt;&lt;/td&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;sha256 of the &lt;strong&gt;sorted&lt;/strong&gt; ciphers, truncated to 12&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;e5627efa2ab1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;hash&lt;/td&gt;&lt;td&gt;sha256 of the &lt;strong&gt;sorted&lt;/strong&gt; extensions (minus SNI and ALPN) + sigalgs &lt;strong&gt;in order&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The 15 ciphers, named from the IANA registry: &lt;code&gt;002f&lt;/code&gt; and &lt;code&gt;0035&lt;/code&gt; (RSA CBC), &lt;code&gt;009c&lt;/code&gt; and &lt;code&gt;009d&lt;/code&gt; (RSA GCM), &lt;code&gt;1301&lt;/code&gt;/&lt;code&gt;1302&lt;/code&gt;/&lt;code&gt;1303&lt;/code&gt; (the three TLS 1.3 suites), &lt;code&gt;c013&lt;/code&gt;/&lt;code&gt;c014&lt;/code&gt; (ECDHE_RSA CBC), &lt;code&gt;c02b&lt;/code&gt;/&lt;code&gt;c02c&lt;/code&gt; (ECDHE_ECDSA GCM), &lt;code&gt;c02f&lt;/code&gt;/&lt;code&gt;c030&lt;/code&gt; (ECDHE_RSA GCM) and &lt;code&gt;cca8&lt;/code&gt;/&lt;code&gt;cca9&lt;/code&gt; (ECDHE ChaCha20). The classic modern-client mix: the &lt;code&gt;130x&lt;/code&gt; for 1.3 plus a 1.2 basket for talking to old servers.&lt;/p&gt;
&lt;p&gt;The 16 extensions: &lt;code&gt;001b&lt;/code&gt; compress_certificate, &lt;code&gt;0000&lt;/code&gt; SNI, &lt;code&gt;0033&lt;/code&gt; key_share, &lt;code&gt;0010&lt;/code&gt; ALPN, &lt;code&gt;4469&lt;/code&gt; ALPS (the old codepoint), &lt;code&gt;0017&lt;/code&gt; extended_master_secret, &lt;code&gt;002d&lt;/code&gt; psk_key_exchange_modes, &lt;code&gt;000d&lt;/code&gt; sigalgs, &lt;code&gt;0005&lt;/code&gt; status_request, &lt;code&gt;0023&lt;/code&gt; session_ticket, &lt;code&gt;0012&lt;/code&gt; SCT, &lt;code&gt;002b&lt;/code&gt; supported_versions, &lt;code&gt;ff01&lt;/code&gt; renegotiation_info, &lt;code&gt;000b&lt;/code&gt; ec_point_formats, &lt;code&gt;000a&lt;/code&gt; supported_groups, &lt;code&gt;0015&lt;/code&gt; padding.&lt;/p&gt;
&lt;p&gt;Recomputing with &lt;code&gt;sha256sum&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sha256(&quot;002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9&quot;)[:12]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  = 8daaf6152771&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sha256(&quot;0005,000a,000b,000d,0012,0015,0017,001b,0023,002b,002d,0033,4469,ff01_0403,0804,0401,0503,0805,0501,0806,0601&quot;)[:12]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  = e5627efa2ab1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Two details that are worth more than memorising the rules. First: SNI and ALPN are &lt;strong&gt;excluded&lt;/strong&gt; from &lt;code&gt;c&lt;/code&gt; on purpose, because they are already in &lt;code&gt;a&lt;/code&gt;. So the same client produces the same &lt;code&gt;c&lt;/code&gt; whether it goes to a domain or an IP. Second: sigalgs go in &lt;strong&gt;in the order they appear, unsorted&lt;/strong&gt;, glued on after a &lt;code&gt;_&lt;/code&gt;. Sort them and the hash changes; I watched that mistake appear in a lab, and FoxIO publishes the vector that gives it away (&lt;code&gt;0403,0804,0401,…&lt;/code&gt;, unsorted).&lt;/p&gt;
&lt;aside class=&quot;markdown-alert markdown-alert-note&quot; dir=&quot;auto&quot;&gt;
&lt;p class=&quot;markdown-alert-title&quot; dir=&quot;auto&quot;&gt;&lt;svg class=&quot;octicon&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;NOTE&lt;/p&gt;
&lt;p&gt;The specification itself carries two incompatible examples: at the top of the document the fingerprint appears as &lt;code&gt;…_b186095e22b6&lt;/code&gt;, while the worked example, with the inputs published, gives &lt;code&gt;…_e5627efa2ab1&lt;/code&gt;. I recomputed it: the second one is right. A good reminder that even a spec has to be checked with &lt;code&gt;sha256sum&lt;/code&gt; before it becomes truth.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;Now the reveal, comparing the spec example with my measured Chrome 156:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;spec       t13d1516h2_8daaf6152771_e5627efa2ab1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Chrome 156 t13d1517h2_8daaf6152771_cb7bf5808d99&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;b&lt;/code&gt; is &lt;strong&gt;identical&lt;/strong&gt;. Chrome’s cipher list did not change between those builds. What changed were the extensions: one more (&lt;code&gt;17&lt;/code&gt; against &lt;code&gt;16&lt;/code&gt;), and &lt;code&gt;c&lt;/code&gt; moved with it. That proves the algorithm is doing its job: the stable part stayed stable, the part that changed changed.&lt;/p&gt;
&lt;h2 id=&quot;sorting-is-a-design-decision&quot;&gt;Sorting is a design decision&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#sorting-is-a-design-decision&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;What I find most elegant about JA4 is not the format, it is the choice of &lt;strong&gt;where&lt;/strong&gt; to sort. I built the minimal case: the same set of extensions, in two orders.&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;order A: 10-11-13-16-23-35-43-51  →  JA3 692aefc05b6b8a9afca0d6486e3b15e4&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;order B: 51-43-35-23-16-13-11-10  →  JA3 79e887fad94c5bfc974a532532781ec3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                                     JA4_c 8ccb6ff8ed2a (identical in both)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figure&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;./permutacao-en.svg&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;Permutation diagram: the same list of eight extensions in two orders produces two different JA3 hashes and a single JA4_c&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;same list, two orders, two JA3 and one JA4&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;Two JA3, one JA4. And the rule behind it is not “order doesn’t matter”: it is that you sort what is &lt;strong&gt;semantically irrelevant and actively randomised&lt;/strong&gt;. TLS extensions are both. HTTP header order, on the other hand, is randomised by nobody and carries information — and JA4H, the HTTP client fingerprint from the same family, does &lt;strong&gt;not&lt;/strong&gt; sort headers. JA4S, which looks at the server’s answer, preserves the original order on purpose.&lt;/p&gt;
&lt;p&gt;Three decisions, one ruler. That is what separates a format that ages well from one that has to be replaced within five years.&lt;/p&gt;
&lt;h2 id=&quot;what-ja4-chooses-not-to-see&quot;&gt;What JA4 chooses not to see&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#what-ja4-chooses-not-to-see&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;No fingerprint sees everything, and it is worth being explicit about what is left out, because that is where bad diagnostic decisions come from.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Not in JA4&quot;&gt;







































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Not in JA4&lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;th&gt;Practical consequence&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;random&lt;/code&gt;, session id, &lt;code&gt;key_share&lt;/code&gt; bytes&lt;/td&gt;&lt;td&gt;they are random&lt;/td&gt;&lt;td&gt;the hash is stable; the bytes never are&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;supported_groups&lt;/code&gt;&lt;/td&gt;&lt;td&gt;deliberately outside &lt;code&gt;c&lt;/code&gt;&lt;/td&gt;&lt;td&gt;two clients differing only in a post-quantum group give the same JA4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;the SNI string&lt;/td&gt;&lt;td&gt;only “present or not”&lt;/td&gt;&lt;td&gt;the hostname is not in the hash&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;extension order&lt;/td&gt;&lt;td&gt;sorted&lt;/td&gt;&lt;td&gt;invisible to JA4, visible to JA3&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;cipher order&lt;/td&gt;&lt;td&gt;sorted&lt;/td&gt;&lt;td&gt;you gain stability and lose the preference&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ALPN beyond the first/last char&lt;/td&gt;&lt;td&gt;only 2 characters&lt;/td&gt;&lt;td&gt;&lt;code&gt;http/1.1&lt;/code&gt; and &lt;code&gt;http/1.1,h2&lt;/code&gt; look identical&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;That last one is the most treacherous, and it is in my records: &lt;code&gt;httpx&lt;/code&gt; in HTTP/2 mode offers &lt;strong&gt;&lt;code&gt;http/1.1, h2&lt;/code&gt;&lt;/strong&gt;, the reverse order, and produces exactly the same JA4 (&lt;code&gt;t13d1712h1_…&lt;/code&gt;) as &lt;code&gt;requests&lt;/code&gt;, which only offers &lt;code&gt;http/1.1&lt;/code&gt;. The &lt;code&gt;a&lt;/code&gt; field takes the first and last character of the &lt;strong&gt;first&lt;/strong&gt; value, so &lt;code&gt;http/1.1&lt;/code&gt; becomes &lt;code&gt;h1&lt;/code&gt; and the trailing &lt;code&gt;h2&lt;/code&gt; disappears. A sensor that only looks at JA3/JA4 &lt;strong&gt;cannot tell that client asked for HTTP/2&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And post-quantum deserves a note: Chrome, Go, Node and OpenSSL 4 offer &lt;code&gt;X25519MLKEM768&lt;/code&gt; (&lt;code&gt;0x11ec&lt;/code&gt;, defined in RFC 10024); Java 21 and macOS curl do not. Because JA4 hashes neither groups nor &lt;code&gt;key_share&lt;/code&gt; content (Chrome sends a 1216-byte share, different on every connection), &lt;strong&gt;the same client keeps the same JA4 before and after PQ&lt;/strong&gt;. The instability from that change lives entirely in JA3, which hashes &lt;code&gt;supported_groups&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;it-isnt-only-tls&quot;&gt;It isn’t only TLS&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#it-isnt-only-tls&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you got this far thinking the problem is TLS, the bad news is that there are three more layers. And a fourth thing that is not a layer at all, it is history.&lt;/p&gt;
&lt;figure&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;./camadas-en.svg&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;Diagram of the layers: TCP/IP, TLS, HTTP/2, HTTP and behaviour, with what the edge measures on each and whether it can be forged&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;each layer has an owner · not all accept a disguise&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;HTTP/2.&lt;/strong&gt; The grammar came from an Akamai white paper in 2017 and it is still the one everyone implements: the fingerprint is built from the frames a client sends before any useful request. The notation is &lt;code&gt;S[;]|WU|P[,]#|PS[,]&lt;/code&gt;: SETTINGS parameters with their values, &lt;strong&gt;in the order they appear&lt;/strong&gt;; the WINDOW_UPDATE increment; the priority tuples; and the pseudo-header order, encoded as &lt;code&gt;m&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;s&lt;/code&gt;. The paper measured more than 10 million connections and, &lt;strong&gt;in 2017&lt;/strong&gt;, published each browser’s order: Chrome &lt;code&gt;m,a,s,p&lt;/code&gt;, Firefox &lt;code&gt;m,p,a,s&lt;/code&gt;, Safari &lt;code&gt;m,s,p,a&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A premise correction, because I have repeated the error myself: &lt;strong&gt;the Akamai format does not include HEADER frame flags&lt;/strong&gt;. The three flows are SETTINGS, WINDOW_UPDATE and PRIORITY, plus pseudo-header order. If you see someone attributing HEADER flags to Akamai, the source does not exist.&lt;/p&gt;
&lt;p&gt;But 2017 is 2017. I measured the clients on this machine, with ALPN negotiating h2 on every connection so nothing degrades silently. Two of them, in the format above:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Chrome 156   1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Java 21      1:16384;2:0;3:0;4:16777216;5:16384;6:393216|33488897|0|a,m,p,s&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Chrome’s &lt;code&gt;PS&lt;/code&gt; is &lt;strong&gt;still&lt;/strong&gt; &lt;code&gt;m,a,s,p&lt;/code&gt;: the 2017 row survived. Go is still &lt;code&gt;a,m,p,s&lt;/code&gt;, matching the paper. And &lt;code&gt;curl&lt;/code&gt; &lt;strong&gt;moved&lt;/strong&gt;: it was &lt;code&gt;m,p,s,a&lt;/code&gt; on 7.54 and became &lt;code&gt;m,s,a,p&lt;/code&gt; on 8.x. That is why you pin the version next to every fingerprint in a table; without it, the table goes stale without warning.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;P&lt;/code&gt; field is dead. Across twelve connections, zero PRIORITY frames: &lt;code&gt;P=0&lt;/code&gt; in all of them. The paper’s &lt;code&gt;P&lt;/code&gt; was built around Firefox 53’s reserved-stream priorities; today’s Chrome sends the deprecated priority block inside HEADERS plus an RFC 9218 &lt;code&gt;priority&lt;/code&gt; header, and the grammar encodes neither. So &lt;code&gt;P=0&lt;/code&gt; means “no PRIORITY frame”, not “no priorities”. It is an honest measure of what a 2017 format loses in 2026.&lt;/p&gt;
&lt;p&gt;And the lesson from the TLS section repeats one layer up: the two &lt;code&gt;curl&lt;/code&gt; on this machine send the same &lt;code&gt;WINDOW_UPDATE&lt;/code&gt; (&lt;code&gt;1048510465&lt;/code&gt;) and the same SETTINGS order (&lt;code&gt;3;4;2&lt;/code&gt;), but Homebrew sends &lt;code&gt;INITIAL_WINDOW_SIZE=65536&lt;/code&gt; while the macOS one sends &lt;code&gt;10485760&lt;/code&gt;. Same tool, different build, different string.&lt;/p&gt;
&lt;p&gt;This matters because a proxy that terminates the connection (and Burp terminates it) builds its own SETTINGS, not the browser’s. And &lt;code&gt;curl_cffi&lt;/code&gt; shows the limit of all of it: it ties Chrome on TLS (identical JA4) and on h2 (identical string), and still gets caught one layer up: the measured Chrome 156 sends &lt;code&gt;HeadlessChrome/156&lt;/code&gt; with 17 headers, &lt;code&gt;curl_cffi&lt;/code&gt; sends &lt;code&gt;Chrome/152&lt;/code&gt; on Windows with 18, including &lt;code&gt;cache-control&lt;/code&gt;. An impersonator has to be right at every layer, and the layers are ordered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HTTP headers.&lt;/strong&gt; Order, casing and defaults. JA4H covers this by hashing the &lt;em&gt;names&lt;/em&gt; in the order they appear. The hygiene almost everyone forgets is this: if you forge the User-Agent to “Chrome 131” but leave &lt;code&gt;sec-ch-ua&lt;/code&gt; saying another version, you did not become more browser-like. You became &lt;strong&gt;inconsistent&lt;/strong&gt;, which is worse than the original.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TCP/IP.&lt;/strong&gt; TTL, window, MSS and option order come from the kernel, not from your client. p0f has been doing this off a single SYN forever, and the modern equivalent is JA4T, in the format &lt;code&gt;window_options_mss_wscale&lt;/code&gt; (FoxIO’s example: &lt;code&gt;64240_2-1-3-1-1-4_1460_8&lt;/code&gt;). It is the layer you cannot fix with a client flag — and why spoofing TLS from a VPS still tells a different story.&lt;/p&gt;
&lt;h2 id=&quot;a-fingerprint-is-a-key-not-a-verdict&quot;&gt;A fingerprint is a key, not a verdict&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#a-fingerprint-is-a-key-not-a-verdict&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The most common conceptual mistake is assuming the edge decides by looking at a hash. It does not, and Cloudflare documents the opposite: alongside JA4 it computes what it calls &lt;strong&gt;JA4 Signals&lt;/strong&gt;: aggregate statistics over the last hour, globally. The names are explicit: &lt;code&gt;browser_ratio_1h&lt;/code&gt;, &lt;code&gt;h2h3_ratio_1h&lt;/code&gt;, &lt;code&gt;reqs_quantile_1h&lt;/code&gt;, &lt;code&gt;uas_rank_1h&lt;/code&gt;, &lt;code&gt;paths_rank_1h&lt;/code&gt;, &lt;code&gt;cache_ratio_1h&lt;/code&gt;, &lt;code&gt;ips_rank_1h&lt;/code&gt;, among others.&lt;/p&gt;
&lt;p&gt;JA4 is the &lt;strong&gt;key&lt;/strong&gt;. What carries weight is how that key is distributed. A vanishingly rare JA4 claiming to be Chrome, with a low &lt;code&gt;browser_ratio_1h&lt;/code&gt; and high volume, is a lost cause, and no cipher adjustment fixes it. Worse: those signals are analytics, not rules (“They cannot be used as filter fields in WAF custom rule expressions”), so you cannot even simulate the decision by looking only at the hash.&lt;/p&gt;
&lt;p&gt;And there is one confusion that costs hours: &lt;strong&gt;a block and a challenge are not the same thing&lt;/strong&gt;. A challenge comes back as HTML, with the header &lt;code&gt;cf-mitigated: challenge&lt;/code&gt;, and exists to run JavaScript in your browser. A &lt;code&gt;cf_clearance&lt;/code&gt; obtained there is bound to the device that solved it, and solving from one IP while using it from another invalidates the solve. If that is your case, the problem is not TLS at all, and no impersonator will fix it.&lt;/p&gt;
&lt;h2 id=&quot;who-computes-this-today&quot;&gt;Who computes this today&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#who-computes-this-today&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is not a research niche. It is something you can write a rule against in production:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Where&quot;&gt;







































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Where&lt;/th&gt;&lt;th&gt;What&lt;/th&gt;&lt;th&gt;Since&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Cloudflare&lt;/td&gt;&lt;td&gt;JA4 in Bot Management (Enterprise), field &lt;code&gt;cf.bot_management.ja4&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JA4+Signals announced Aug 2024&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AWS WAF&lt;/td&gt;&lt;td&gt;JA3&lt;/td&gt;&lt;td&gt;Sep 2023&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;AWS WAF&lt;/td&gt;&lt;td&gt;JA4, including as a rate-based rule key&lt;/td&gt;&lt;td&gt;Mar 2025&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Google Cloud Armor&lt;/td&gt;&lt;td&gt;&lt;code&gt;origin.tls_ja4_fingerprint&lt;/code&gt; and &lt;code&gt;origin.tls_ja3_fingerprint&lt;/code&gt;&lt;/td&gt;&lt;td&gt;documented today&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Akamai&lt;/td&gt;&lt;td&gt;JA4 (beta, via injected header)&lt;/td&gt;&lt;td&gt;2026&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Fastly&lt;/td&gt;&lt;td&gt;VCL variable &lt;code&gt;tls.client.ja4&lt;/code&gt; and NGWAF&lt;/td&gt;&lt;td&gt;Feb 2025&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Across the 38 connections, the 10 stacks produced &lt;strong&gt;24 distinct JA4&lt;/strong&gt;, and three different clients collapsed onto the same one (&lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;httpx&lt;/code&gt; in h1 and in h2). That sums up the state of the art: JA4 is good at saying “what kind of software is this”, and insufficient to say “which tool exactly”. For that you need the next layer.&lt;/p&gt;
&lt;h2 id=&quot;what-i-take-from-this&quot;&gt;What I take from this&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#what-i-take-from-this&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Four things, and none of them is “use tool X”.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The block happens before the payload.&lt;/strong&gt; If your encoding, your header and your token are right and you still get a 403, the answer was probably decided in the handshake. Staring at the body at that point is staring at the wrong place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The stack is the signal, not the language.&lt;/strong&gt; Two &lt;code&gt;curl&lt;/code&gt; on the same machine, different fingerprints. A library upgrade changes your identity on the network, and nobody tells you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Browser JA3 died in 2023, on purpose.&lt;/strong&gt; It is not a bug and not a maintenance gap: it is an engineering decision by Chrome that Fastly measured and Cloudflare commented on. Anyone still building detection on browser JA3 is tracking a target that moves on every connection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;And the fingerprint is never the whole story.&lt;/strong&gt; It is one key in a decision that also weighs prevalence, ASN, rate, cookie and behaviour. Confusing “my TLS is wrong” with “my IP is burned” is the fastest way to spend the whole afternoon tuning ciphers without changing anything.&lt;/p&gt;
&lt;p&gt;In the next post I take the opposite route: I pick up Burp, show across three layers why it announces itself as a Java tool, and what can be done about it — including what cannot, and why.&lt;/p&gt;</content:encoded><category>tls</category><category>fingerprint</category><category>ja3</category><category>ja4</category><category>cloudflare</category></item><item><title>omp: from zero to /vibe</title><link>https://flameee-sec.netlify.app/posts/configuring-omp/</link><guid isPermaLink="true">https://flameee-sec.netlify.app/posts/configuring-omp/</guid><description>Install, theme, providers, models and /vibe: a walkthrough of my omp setup on macOS, Linux and Windows.</description><pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;First real post here, and it’s the tutorial I wish I’d had: going from zero to my &lt;strong&gt;omp&lt;/strong&gt; (&lt;a href=&quot;https://github.com/can1357/oh-my-pi&quot;&gt;oh-my-pi&lt;/a&gt;) setup, configured and good-looking, step by step. Everything was checked against omp’s docs and CLI (18.12.5 here; the Linux path was tested in a clean Ubuntu 24.04 container, which installed 18.12.6).&lt;/p&gt;
&lt;aside class=&quot;markdown-alert markdown-alert-note&quot; dir=&quot;auto&quot;&gt;
&lt;p class=&quot;markdown-alert-title&quot; dir=&quot;auto&quot;&gt;&lt;svg class=&quot;octicon&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;NOTE&lt;/p&gt;
&lt;p&gt;This guide covers &lt;strong&gt;macOS&lt;/strong&gt;, &lt;strong&gt;Linux&lt;/strong&gt; (Debian/Ubuntu) and &lt;strong&gt;Windows&lt;/strong&gt;. On Windows, omp runs natively in PowerShell, per the official README; Oh My Zsh and Powerlevel10k only exist inside WSL2. Steps that are identical on every OS get a single block.&lt;/p&gt;
&lt;/aside&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Step&quot;&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Step&lt;/th&gt;&lt;th&gt;macOS&lt;/th&gt;&lt;th&gt;Linux (Debian/Ubuntu)&lt;/th&gt;&lt;th&gt;Windows&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Package manager&lt;/td&gt;&lt;td&gt;Homebrew&lt;/td&gt;&lt;td&gt;apt&lt;/td&gt;&lt;td&gt;winget&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Terminal&lt;/td&gt;&lt;td&gt;Warp (mine)&lt;/td&gt;&lt;td&gt;Warp or any terminal with a Nerd Font&lt;/td&gt;&lt;td&gt;Windows Terminal (or Warp)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Nerd Font&lt;/td&gt;&lt;td&gt;&lt;code&gt;brew install --cask&lt;/code&gt;&lt;/td&gt;&lt;td&gt;zip into &lt;code&gt;~/.local/share/fonts&lt;/code&gt; + &lt;code&gt;fc-cache&lt;/code&gt;&lt;/td&gt;&lt;td&gt;zip → Install&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Shell theme&lt;/td&gt;&lt;td&gt;Oh My Zsh + p10k&lt;/td&gt;&lt;td&gt;Oh My Zsh + p10k&lt;/td&gt;&lt;td&gt;inside WSL2 only&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;omp&lt;/td&gt;&lt;td&gt;&lt;code&gt;brew install can1357/tap/omp&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;curl -fsSL https://omp.sh/install | sh&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;irm https://omp.sh/install.ps1 | iex&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;what-a-harness-is&quot;&gt;What a harness is&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#what-a-harness-is&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you’ve only used LLMs through a chat window, this is the missing piece. A model on its own is text in, text out. It doesn’t read your repository, doesn’t run commands, doesn’t remember last week. The &lt;strong&gt;harness&lt;/strong&gt; is the program around it that turns it into an agent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The agent loop.&lt;/strong&gt; The model asks for a tool call, the harness runs it and feeds the result back, and this repeats until the task is done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The tools.&lt;/strong&gt; Reading and editing files, a shell, search, a browser, MCP servers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What goes into the context.&lt;/strong&gt; The system prompt, project rules (&lt;code&gt;AGENTS.md&lt;/code&gt;), skills, memory, file contents, and compaction when the context fills up.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permissions and approvals.&lt;/strong&gt; What runs on its own and what needs your “ok”.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sessions and history.&lt;/strong&gt; Picking up where you left off.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sub-agents and orchestration.&lt;/strong&gt; Splitting work across several agents (in omp, that includes &lt;code&gt;/vibe&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Model routing.&lt;/strong&gt; A strong model to think, a cheap one for the grunt work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of a console: the model is the CPU; the harness is everything else, the controller, the memory card and the disc drive.&lt;/p&gt;
&lt;p&gt;In practice, the agent &lt;strong&gt;acts&lt;/strong&gt; instead of suggesting: it runs the command and reads the output, no copy-pasting on your end. It &lt;strong&gt;keeps state&lt;/strong&gt; across a long task. And rules, skills and project context make the agent follow &lt;strong&gt;your method&lt;/strong&gt; every time, without you pasting the same instructions into every conversation. Parallel work becomes sub-agents or workers. In my experience, the same model feels like a different one depending on the harness’s tools and how it handles context.&lt;/p&gt;
&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; do: it doesn’t replace judgement, and it doesn’t make a bad model good.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Aspect&quot;&gt;


































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Aspect&lt;/th&gt;&lt;th&gt;Chat&lt;/th&gt;&lt;th&gt;Harness&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Runs commands&lt;/td&gt;&lt;td&gt;no&lt;/td&gt;&lt;td&gt;yes, with configurable approval&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Edits files&lt;/td&gt;&lt;td&gt;you copy and paste&lt;/td&gt;&lt;td&gt;straight to disk&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Project rules&lt;/td&gt;&lt;td&gt;you paste them every time&lt;/td&gt;&lt;td&gt;loaded automatically&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Parallel agents&lt;/td&gt;&lt;td&gt;no&lt;/td&gt;&lt;td&gt;sub-agents and workers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Model choice&lt;/td&gt;&lt;td&gt;whatever the app offers&lt;/td&gt;&lt;td&gt;any provider, one per job&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Examples of the category: Claude Code, Codex CLI, opencode, Aider, Cursor’s agent, and omp.&lt;/p&gt;
&lt;h3 id=&quot;why-omp&quot;&gt;Why omp&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#why-omp&quot;&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;For me, omp wins by putting all of that in one place: dozens of providers (via environment variables, OAuth login or any compatible gateway), local models detected automatically, &lt;strong&gt;model roles&lt;/strong&gt; with a thinking level per role, sub-agents you can customise in markdown, &lt;strong&gt;&lt;code&gt;/vibe&lt;/code&gt;&lt;/strong&gt;, skills, memory across sessions, MCP, approval modes, secret obfuscation, isolated profiles and resumable sessions. And it looks good in the terminal, which is half of this post.&lt;/p&gt;
&lt;h2 id=&quot;1-prerequisites-and-terminal&quot;&gt;1. Prerequisites and terminal&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#1-prerequisites-and-terminal&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;omp needs no runtime: Homebrew and the official scripts ship a prebuilt binary. Bun (≥ 1.3.14, per the README) only matters if you’d rather install with &lt;code&gt;bun install -g @oh-my-pi/pi-coding-agent&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The terminal has to accept a custom font. On macOS I use &lt;a href=&quot;https://www.warp.dev/download&quot;&gt;Warp&lt;/a&gt;, which also has Linux and Windows builds. On Windows, the sensible default is Windows Terminal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS&lt;/strong&gt; (Homebrew, official command from &lt;a href=&quot;https://brew.sh&quot;&gt;brew.sh&lt;/a&gt;, and Warp)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;/bin/bash&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;brew&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; --cask&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; warp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Linux&lt;/strong&gt; (the basics the next steps use; terminal: Warp from its site, or whatever you already use)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; apt&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; update&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; sudo&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; apt&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -y&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; curl&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; git&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; unzip&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; fontconfig&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Windows&lt;/strong&gt; (PowerShell; Windows Terminal is also on the Microsoft Store)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;POWERSHELL&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;winget install &lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;--&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;id Microsoft.WindowsTerminal &lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;e&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;2-nerd-font&quot;&gt;2. Nerd Font&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#2-nerd-font&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A Nerd Font is a programming font patched with thousands of extra icons: the git logo, folders, powerline arrows, spinners. omp uses those icons when you turn on &lt;code&gt;symbolPreset: nerd&lt;/code&gt;, its status line has a &lt;code&gt;nerd&lt;/code&gt; preset, and the Powerlevel10k prompt in step 3 relies on them too. Without the font, the icons turn into little boxes.&lt;/p&gt;
&lt;p&gt;I use &lt;strong&gt;JetBrains Mono Nerd Font&lt;/strong&gt;, in the &lt;strong&gt;NL&lt;/strong&gt; variant (no ligatures), whose family name is &lt;code&gt;JetBrainsMonoNL Nerd Font&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;brew&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; --cask&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; font-jetbrains-mono-nerd-font&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Linux&lt;/strong&gt; (the nerd-fonts project’s manual method; &lt;code&gt;fc-list&lt;/code&gt; confirms it)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;mkdir&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -p&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; ~/.local/share/fonts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -fsSLo&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; /tmp/JetBrainsMono.zip&lt;/span&gt;&lt;span style=&quot;color:#FF7BD0&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;  https://github.com/ryanoasis/nerd-fonts/releases/latest/download/JetBrainsMono.zip&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;unzip&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -oq&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; /tmp/JetBrainsMono.zip&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -d&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; ~/.local/share/fonts/JetBrainsMono&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;fc-cache&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -fv&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;fc-list&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; grep&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -i&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;JetBrainsMonoNL Nerd Font&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On Arch, the official package is &lt;code&gt;sudo pacman -S ttf-jetbrains-mono-nerd&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows&lt;/strong&gt; (download &lt;code&gt;JetBrainsMono.zip&lt;/code&gt; from &lt;a href=&quot;https://github.com/ryanoasis/nerd-fonts/releases/latest&quot;&gt;github.com/ryanoasis/nerd-fonts/releases/latest&lt;/a&gt;, extract it, select the &lt;code&gt;.ttf&lt;/code&gt; files and right-click → &lt;strong&gt;Install&lt;/strong&gt;). Then, in Windows Terminal’s &lt;code&gt;settings.json&lt;/code&gt; (Settings with &lt;kbd&gt;Shift&lt;/kbd&gt; held), set the font for every profile under &lt;code&gt;profiles.defaults&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;JSON&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;profiles&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;defaults&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;font&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;face&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;JetBrainsMonoNL Nerd Font&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;size&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 13&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you use WSL2, install the font on &lt;strong&gt;Windows&lt;/strong&gt;: Windows Terminal draws the icons, not Linux.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pick it in the terminal.&lt;/strong&gt; In Warp (any OS), choose &lt;code&gt;JetBrainsMonoNL Nerd Font&lt;/code&gt; in the appearance settings. On macOS my &lt;code&gt;~/.warp/settings.toml&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TOML&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;toml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6;font-weight:bold&quot;&gt;appearance&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6;font-weight:bold&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;font_family &lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;JetBrainsMonoNL Nerd Font&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;font_size &lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 13.0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6;font-weight:bold&quot;&gt;appearance&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6;font-weight:bold&quot;&gt;themes&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;theme &lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;dark&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6;font-weight:bold&quot;&gt;appearance&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6;font-weight:bold&quot;&gt;cursor&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;cursor_display_type &lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;bar&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In any other terminal (iTerm2, Ghostty, Kitty, GNOME Terminal…) the principle is the same: the terminal font has to be a Nerd Font.&lt;/p&gt;
&lt;h2 id=&quot;3-the-shells-look-optional&quot;&gt;3. The shell’s look (optional)&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#3-the-shells-look-optional&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I use Oh My Zsh with the Powerlevel10k theme. omp doesn’t need it, but it’s part of how my terminal looks. Zsh, Oh My Zsh and p10k are Unix-shell things: on Windows they work inside WSL2, not in PowerShell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; zsh is already the default shell; nothing to install.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux&lt;/strong&gt; (and Windows inside WSL2: install it with &lt;code&gt;wsl --install&lt;/code&gt; in an administrator PowerShell, reboot, open Ubuntu and continue from here)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;sudo&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; apt&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -y&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; zsh&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; git&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; curl&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;chsh&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -s&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;which&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; zsh&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt;   # takes effect on your next login&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;macOS, Linux and WSL2:&lt;/strong&gt; from here on it’s the same. Oh My Zsh, then Powerlevel10k as its theme, with the commands from the official READMEs:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;sh&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -c&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$(&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; clone&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; --depth=1&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; https://github.com/romkatv/powerlevel10k.git&lt;/span&gt;&lt;span style=&quot;color:#FF7BD0&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;${&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;ZSH_CUSTOM&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:-$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOME&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;.oh-my-zsh&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;custom&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;/themes/powerlevel10k&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In &lt;code&gt;~/.zshrc&lt;/code&gt;, switch the theme (I keep only the &lt;code&gt;git&lt;/code&gt; plugin):&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;ZSH_THEME&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;&quot;powerlevel10k/powerlevel10k&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;plugins&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=(&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;git&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Open a new terminal; the wizard starts on its own. If it doesn’t, run &lt;code&gt;p10k configure&lt;/code&gt;. My picks were &lt;code&gt;nerdfont-v3 + powerline&lt;/code&gt;, small icons, &lt;code&gt;rainbow&lt;/code&gt; style, 24h clock, angled separators, two lines, transient prompt and instant prompt. The wizard writes everything to &lt;code&gt;~/.p10k.zsh&lt;/code&gt;. In Warp, the p10k prompt only shows up with &lt;code&gt;honor_ps1 = true&lt;/code&gt; under &lt;code&gt;[terminal.input]&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;4-installing-omp&quot;&gt;4. Installing omp&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#4-installing-omp&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;macOS&lt;/strong&gt; (how I install it)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;brew&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; can1357/tap/omp&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Linux&lt;/strong&gt; (official script; installs to &lt;code&gt;~/.local/bin&lt;/code&gt;, which has to be on your &lt;code&gt;PATH&lt;/code&gt;)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -fsSL&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; https://omp.sh/install&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; |&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; sh&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt; PATH&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOME/.local/bin:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;PATH&quot;&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt;   # put this in ~/.zshrc or ~/.bashrc&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Windows&lt;/strong&gt; (PowerShell, official script; inside WSL2, use the Linux command)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;POWERSHELL&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;irm https:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;//&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;omp.sh&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;/&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;install.ps1 &lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt; iex&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On any OS, check with &lt;code&gt;omp --version&lt;/code&gt;. &lt;code&gt;omp setup&lt;/code&gt; runs the onboarding wizard; after that, just run &lt;code&gt;omp&lt;/code&gt; inside a project. Everything user-level lives in &lt;code&gt;~/.omp/agent/&lt;/code&gt; (&lt;code&gt;omp config path&lt;/code&gt; shows where): &lt;code&gt;config.yml&lt;/code&gt;, &lt;code&gt;models.yml&lt;/code&gt;, &lt;code&gt;mcp.json&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;skills/&lt;/code&gt;, &lt;code&gt;agents/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A provider.&lt;/strong&gt; OAuth (subscription) is the same everywhere: &lt;code&gt;omp login&lt;/code&gt; and pick from the list (inside a session, &lt;code&gt;/login&lt;/code&gt;). For an API key, keep it out of your shell history:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;macOS and Linux&lt;/strong&gt; (works in zsh and bash)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;printf&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &apos;API key: &apos;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; read&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -rs&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; K&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; printf&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &apos;\nANTHROPIC_API_KEY=%s\n&apos;&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;K&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; ~/.omp/agent/.env&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; unset&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; K&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;chmod&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 600&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; ~/.omp/agent/.env&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;omp&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; models&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt;   # what&apos;s available now, grouped by provider&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Windows&lt;/strong&gt; (PowerShell 7.1+; stores a user environment variable that applies to new terminals)&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;POWERSHELL&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;powershell&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;k &lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; Read-Host&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &apos;API key&apos;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;MaskInput&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;Environment&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;]&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;::SetEnvironmentVariable&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;&apos;ANTHROPIC_API_KEY&apos;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;k&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &apos;User&apos;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;Remove-Variable&lt;/span&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt; k&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;omp reads &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, &lt;code&gt;GEMINI_API_KEY&lt;/code&gt;, &lt;code&gt;OPENROUTER_API_KEY&lt;/code&gt;, &lt;code&gt;DEEPSEEK_API_KEY&lt;/code&gt; and many more; &lt;code&gt;omp --help&lt;/code&gt; lists the main ones.&lt;/p&gt;
&lt;h2 id=&quot;5-making-omp-look-good&quot;&gt;5. Making omp look good&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#5-making-omp-look-good&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;From here to the end, everything is &lt;strong&gt;the same on all three OSes&lt;/strong&gt;: it’s omp config, not OS config. The look lives in &lt;code&gt;~/.omp/agent/config.yml&lt;/code&gt;. These are the values I changed from the defaults:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;YAML&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt;# ~/.omp/agent/config.yml — appearance&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;theme&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  dark&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; dark-dracula&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # built-in theme (default: titanium)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;symbolPreset&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; nerd&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # Nerd Font icons (default: unicode)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;statusLine&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  preset&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; nerd&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # icons in the status line (default: default)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  contextLine&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; annotated&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # annotated context usage&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;composer&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  shape&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; box&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # editor in a box (default: band)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;display&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  showTokenUsage&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # tokens per reply&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  showTurnTime&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # total time per turn, tool calls included&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or from the CLI, one key at a time (the name has to match the schema exactly):&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;omp&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; theme.dark&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; dark-dracula&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;omp&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; symbolPreset&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; nerd&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;omp&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; statusLine.preset&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; nerd&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;omp&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; statusLine.contextLine&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; annotated&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;omp&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; composer.shape&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; box&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;omp&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; display.showTokenUsage&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;omp&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; config&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; set&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; display.showTurnTime&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;All of this is also in &lt;code&gt;/settings&lt;/code&gt; inside a session, if you prefer a menu. The other &lt;code&gt;statusLine.preset&lt;/code&gt; options are &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;minimal&lt;/code&gt;, &lt;code&gt;compact&lt;/code&gt;, &lt;code&gt;full&lt;/code&gt;, &lt;code&gt;ascii&lt;/code&gt; and &lt;code&gt;custom&lt;/code&gt;. The separator lives in &lt;code&gt;statusLine.separator&lt;/code&gt; (&lt;code&gt;powerline&lt;/code&gt;, &lt;code&gt;powerline-thin&lt;/code&gt;, &lt;code&gt;slash&lt;/code&gt;, &lt;code&gt;pipe&lt;/code&gt;, &lt;code&gt;block&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;ascii&lt;/code&gt;). Custom themes are JSON files in &lt;code&gt;~/.omp/agent/themes/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Expected result:&lt;/strong&gt; when you open &lt;code&gt;omp&lt;/code&gt;, the editor sits in a box, the status line shows the model, folder and git state with icons, and the colours are Dracula’s. If you see little boxes, the font from step 2 isn’t selected.&lt;/p&gt;
&lt;h2 id=&quot;6-settings-worth-changing-beyond-looks&quot;&gt;6. Settings worth changing beyond looks&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#6-settings-worth-changing-beyond-looks&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Mine, all of them off the defaults:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Key&quot;&gt;



























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Key&lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;th&gt;What it does&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;defaultThinkingLevel&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;max&lt;/code&gt;&lt;/td&gt;&lt;td&gt;default reasoning depth (default: &lt;code&gt;high&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;0.7&lt;/code&gt;&lt;/td&gt;&lt;td&gt;sampling temperature (default: the provider’s)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory.backend&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;local&lt;/code&gt;&lt;/td&gt;&lt;td&gt;extracts lessons from sessions and injects a summary into the next one (default: &lt;code&gt;off&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;autolearn.enabled&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;lets the agent create and update managed skills&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;autolearn.autoContinue&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;runs a private lesson-capture turn when it stops; costs tokens&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;model.loopGuard.enabled&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&lt;td&gt;turns off automatic loop detection on reasoning and prose&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;model.toolCallLoopGuard.threshold&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;20&lt;/code&gt;&lt;/td&gt;&lt;td&gt;identical calls in a row before it steps in (default: &lt;code&gt;5&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;bash.allowCompoundCommands&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;evaluates each command in an &lt;code&gt;&amp;#x26;&amp;#x26;&lt;/code&gt; chain separately&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;browser.headless&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&lt;td&gt;shows the window when the agent uses the browser&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;computer.enabled&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;enables desktop control (screenshots, input)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Two I recommend to everyone, even though they’re not on my list:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;secrets.enabled: true&lt;/code&gt;&lt;/strong&gt; (default &lt;code&gt;false&lt;/code&gt;): credential-looking env vars and known patterns (JWTs, Bearer headers, cloud keys) become placeholders in the text sent to the provider and are restored right before a tool runs. It doesn’t catch short values or the static prompt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tools.approvalMode&lt;/code&gt;&lt;/strong&gt;: the default is &lt;code&gt;yolo&lt;/code&gt; (everything runs). If you’re starting out, &lt;code&gt;write&lt;/code&gt; approves reads and writes and asks before executing. Sub-agents always run in &lt;code&gt;yolo&lt;/code&gt;; the approval that counts is the parent’s.&lt;/li&gt;
&lt;/ul&gt;
&lt;aside class=&quot;markdown-alert markdown-alert-tip&quot; dir=&quot;auto&quot;&gt;
&lt;p class=&quot;markdown-alert-title&quot; dir=&quot;auto&quot;&gt;&lt;svg class=&quot;octicon&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;TIP&lt;/p&gt;
&lt;p&gt;&lt;code&gt;omp --profile hunt --alias omp-hunt&lt;/code&gt; creates an isolated profile (its own auth, sessions and settings) plus a shell shortcut for it. It’s a good way to keep a more aggressive setup apart from your everyday omp.&lt;/p&gt;
&lt;/aside&gt;
&lt;h2 id=&quot;7-providers-and-models&quot;&gt;7. Providers and models&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#7-providers-and-models&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;omp works with &lt;strong&gt;roles&lt;/strong&gt;, set under &lt;code&gt;modelRoles&lt;/code&gt; in &lt;code&gt;config.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Role&quot;&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Role&lt;/th&gt;&lt;th&gt;Used for&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;default&lt;/code&gt;&lt;/td&gt;&lt;td&gt;the main conversation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;smol&lt;/code&gt;&lt;/td&gt;&lt;td&gt;light tasks; &lt;code&gt;/vibe&lt;/code&gt; &lt;code&gt;fast&lt;/code&gt; workers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;slow&lt;/code&gt;&lt;/td&gt;&lt;td&gt;heavy reasoning&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;plan&lt;/code&gt;&lt;/td&gt;&lt;td&gt;plan mode (&lt;code&gt;/plan&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;task&lt;/code&gt;&lt;/td&gt;&lt;td&gt;sub-agents and &lt;code&gt;/vibe&lt;/code&gt; &lt;code&gt;good&lt;/code&gt; workers&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;vision&lt;/code&gt;&lt;/td&gt;&lt;td&gt;image analysis&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;commit&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;omp commit&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;tiny&lt;/code&gt; / &lt;code&gt;memory&lt;/code&gt;&lt;/td&gt;&lt;td&gt;session titles and the memory pipeline&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;A &lt;code&gt;:level&lt;/code&gt; suffix sets the thinking level per role (&lt;code&gt;off&lt;/code&gt;, &lt;code&gt;minimal&lt;/code&gt;, &lt;code&gt;low&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;xhigh&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;auto&lt;/code&gt;): &lt;code&gt;default: deepseek/deepseek-v4-flash:max&lt;/code&gt;. Unset &lt;code&gt;smol&lt;/code&gt; and &lt;code&gt;slow&lt;/code&gt; inherit &lt;code&gt;default&lt;/code&gt;. To switch: &lt;code&gt;/model&lt;/code&gt; opens the hub, &lt;kbd&gt;Ctrl&lt;/kbd&gt;+&lt;kbd&gt;P&lt;/kbd&gt; cycles through the role models, &lt;kbd&gt;Shift&lt;/kbd&gt;+&lt;kbd&gt;Tab&lt;/kbd&gt; cycles the thinking level, and &lt;code&gt;omp --model opus&lt;/code&gt; does fuzzy matching.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your own gateway&lt;/strong&gt; (any OpenAI- or Anthropic-compatible endpoint) goes in &lt;code&gt;~/.omp/agent/models.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;YAML&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;providers&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  my-gateway&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;    baseUrl&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; https://gateway.example/v1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;    api&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; openai-completions&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # or anthropic-messages, openai-responses…&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;    apiKey&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; MY_GATEWAY_API_KEY&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # name of an env var; omp uses its value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;    authHeader&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # Authorization: Bearer &amp;#x3C;key&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;    models&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;      -&lt;/span&gt;&lt;span style=&quot;color:#F0525A&quot;&gt; id&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; vendor/big-model&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt; # the id the endpoint expects&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;        name&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; Big Model&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;        reasoning&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;        thinking&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#F0525A&quot;&gt; mode&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; effort&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#F0525A&quot;&gt; efforts&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;low&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; high&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; max&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;],&lt;/span&gt;&lt;span style=&quot;color:#F0525A&quot;&gt; defaultLevel&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; high&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;        input&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;text&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; image&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;        contextWindow&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 1048576&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;        maxTokens&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 131072&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;apiKey&lt;/code&gt; means “env var name or literal”: if it matches a non-empty variable, that value is used. A YAML error makes omp skip the whole file, so always check with &lt;code&gt;omp models my-gateway&lt;/code&gt;. And before declaring a provider, run &lt;code&gt;omp models &amp;#x3C;name&gt;&lt;/code&gt;: if it’s already built in, all you need is the credential.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I use:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;default&lt;/code&gt; role&lt;/strong&gt;: &lt;code&gt;deepseek/deepseek-v4-flash:max&lt;/code&gt;, through the built-in DeepSeek provider. Everything that inherits &lt;code&gt;default&lt;/code&gt; (the &lt;code&gt;fast&lt;/code&gt; workers, for example) runs on it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Main session and &lt;code&gt;/vibe&lt;/code&gt; director&lt;/strong&gt;: Claude Opus (&lt;code&gt;anthropic/claude-opus-5-5&lt;/code&gt;). omp lets you switch just the session’s model without touching the &lt;code&gt;default&lt;/code&gt; role: &lt;kbd&gt;Alt&lt;/kbd&gt;+&lt;kbd&gt;P&lt;/kbd&gt; picks a temporary model for the session, and &lt;code&gt;omp --model opus&lt;/code&gt; does the same from the command line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A gateway in &lt;code&gt;models.yml&lt;/code&gt;&lt;/strong&gt; with DeepSeek V4.1 Flash, Nemotron 3.5 Super VL 120B and GLM-5.3 Flash, all with a 1M context, their effort ladders declared and costs filled in so omp can track spend.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;8-vibe&quot;&gt;8. /vibe&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#8-vibe&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/vibe&lt;/code&gt; turns the session into a &lt;strong&gt;director&lt;/strong&gt;. The director keeps only &lt;code&gt;read&lt;/code&gt;, an optional &lt;code&gt;todo&lt;/code&gt; and five control tools; the searching, editing and running is done by &lt;strong&gt;persistent workers&lt;/strong&gt; in the background, each with its own transcript.&lt;/p&gt;
&lt;figure&gt;&lt;img __ASTRO_IMAGE_=&quot;{&amp;#x22;src&amp;#x22;:&amp;#x22;./vibe-director.svg&amp;#x22;,&amp;#x22;alt&amp;#x22;:&amp;#x22;/vibe diagram: the director spawns fast and good workers, which work in the workspace; the director verifies by reading the files&amp;#x22;,&amp;#x22;index&amp;#x22;:0}&quot;&gt;&lt;figcaption&gt;The director reads and decides; the workers do.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/vibe                       # enter the mode&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/vibe migrate the parser    # enter and send the first directive&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;/vibe                       # exit (shuts the workers down)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table: Tier&quot;&gt;






















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tier&lt;/th&gt;&lt;th&gt;Agent&lt;/th&gt;&lt;th&gt;Role&lt;/th&gt;&lt;th&gt;Use for&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;fast&lt;/td&gt;&lt;td&gt;sonic&lt;/td&gt;&lt;td&gt;@smol&lt;/td&gt;&lt;td&gt;mechanical execution, volume, drafts&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;good&lt;/td&gt;&lt;td&gt;task&lt;/td&gt;&lt;td&gt;@task&lt;/td&gt;&lt;td&gt;judgement, design, reviewing &lt;code&gt;fast&lt;/code&gt; output&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The director uses &lt;code&gt;vibe_spawn&lt;/code&gt; (creates a worker with a brief and a tier), &lt;code&gt;vibe_send&lt;/code&gt; (steers or sends the next step), &lt;code&gt;vibe_wait&lt;/code&gt;, &lt;code&gt;vibe_kill&lt;/code&gt; and &lt;code&gt;vibe_list&lt;/code&gt;. You just talk to it in plain language.&lt;/p&gt;
&lt;p&gt;To pick the tier models, the docs suggest aliases:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;YAML&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;task&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  agentModelOverrides&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;    sonic&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &apos;@fast_worker&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;    task&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &apos;@good_worker&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;modelRoles&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  fast_worker&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; my-gateway/vendor/fast-model&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F0525A&quot;&gt;  good_worker&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; my-gateway/vendor/big-model:high&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That override applies to every &lt;code&gt;sonic&lt;/code&gt; and &lt;code&gt;task&lt;/code&gt; sub-agent, not just &lt;code&gt;/vibe&lt;/code&gt;. I didn’t configure any of it: &lt;code&gt;fast&lt;/code&gt; inherits &lt;code&gt;default&lt;/code&gt; (DeepSeek V4 Flash) and &lt;code&gt;good&lt;/code&gt; falls back to the director’s active model (Opus). Cheap grunt work, expensive judgement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to brief.&lt;/strong&gt; A worker can’t see the director’s conversation. Give it files, constraints and an acceptance criterion you can check:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Goal: cursor pagination on GET /api/orders.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Files: src/api/orders.ts, tests/orders.test.ts. Don&apos;t touch anything else.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Acceptance: `npm test -- orders` passes, with a new test for the last page.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Deliver: the files you touched and the test output.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;How to verify.&lt;/strong&gt; The docs say it plainly: a worker finishing means the turn ended, not that it got things right. The director reads the touched files and the full output at &lt;code&gt;agent://&amp;#x3C;id&gt;&lt;/code&gt;. One worker per workstream, &lt;code&gt;fast&lt;/code&gt; and &lt;code&gt;good&lt;/code&gt; in parallel, and &lt;code&gt;good&lt;/code&gt; reviewing &lt;code&gt;fast&lt;/code&gt; when it matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt; exiting &lt;code&gt;/vibe&lt;/code&gt; shuts down every worker; it doesn’t mix with plan mode or goal mode; while it’s on, you can’t start, delete, fork or move the session. This post, by the way, was drafted by an omp worker in a director session.&lt;/p&gt;
&lt;h2 id=&quot;9-skills-mcp-and-agentsmd&quot;&gt;9. Skills, MCP and AGENTS.md&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#9-skills-mcp-and-agentsmd&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AGENTS.md&lt;/strong&gt;: omp loads &lt;code&gt;~/.omp/agent/AGENTS.md&lt;/code&gt;, &lt;code&gt;.omp/AGENTS.md&lt;/code&gt;, and a plain &lt;code&gt;AGENTS.md&lt;/code&gt; searched from the current directory up to the repo root (it reads &lt;code&gt;CLAUDE.md&lt;/code&gt; too). Keep it short: hard rules, and where everything else lives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt;: a directory with a &lt;code&gt;SKILL.md&lt;/code&gt; (&lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; in the frontmatter). Only the name and description go into the prompt; the body loads on demand (&lt;code&gt;skill://&amp;#x3C;name&gt;&lt;/code&gt;). Yours live in &lt;code&gt;~/.omp/agent/skills/&lt;/code&gt;; with &lt;code&gt;autolearn&lt;/code&gt;, the agent writes its own into &lt;code&gt;~/.omp/agent/managed-skills/&lt;/code&gt;. I have about 30 of mine (vulnerability classes, methodology, triage, report writing) and dozens of managed ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt;: &lt;code&gt;~/.omp/agent/mcp.json&lt;/code&gt;, with &lt;code&gt;${VAR}&lt;/code&gt; expanded. I use Burp Suite through PortSwigger’s official &lt;a href=&quot;https://github.com/PortSwigger/mcp-server&quot;&gt;MCP Server extension&lt;/a&gt; (local SSE, by default at &lt;code&gt;http://127.0.0.1:9876&lt;/code&gt;), and a private knowledge base, a Python MCP server over my notes:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;JSON&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;  &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;mcpServers&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;burp&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;sse&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;url&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;http://127.0.0.1:9876&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;    &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;kb&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;stdio&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;command&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;${HOME}/kb/.venv/bin/python&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;&quot;${HOME}/kb/server.py&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;      &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#E8A87C&quot;&gt;KB_PATH&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;${HOME}/kb/data&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;10-a-taste-bug-bounty&quot;&gt;10. A taste: bug bounty&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#10-a-taste-bug-bounty&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;aside class=&quot;markdown-alert markdown-alert-warning&quot; dir=&quot;auto&quot;&gt;
&lt;p class=&quot;markdown-alert-title&quot; dir=&quot;auto&quot;&gt;&lt;svg class=&quot;octicon&quot; viewBox=&quot;0 0 16 16&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;WARNING&lt;/p&gt;
&lt;p&gt;Many programs restrict sharing vulnerability or target data with third parties, and a cloud LLM provider can count as a third party. Read the program’s policy first. Prefer providers or settings with no data retention, or local models. When in doubt, don’t send it.&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;For large programs I open several terminals running omp, each with one role, and they talk &lt;strong&gt;only through the filesystem&lt;/strong&gt;. I call it the pack: an &lt;code&gt;orch&lt;/code&gt; owns scope and the queue, attackers &lt;code&gt;a1..aN&lt;/code&gt; take one host at a time, and a &lt;code&gt;triage&lt;/code&gt; validates candidates. Every shared write goes through a bash script, &lt;code&gt;bin/hb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;TEXT&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;~/hunting/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── PROTOCOL.md            # the contract: roles, lease, rate, proof&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── bin/hb                 # the bus&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;├── scope/&amp;#x3C;program&gt;/       # eligible.txt, deny.txt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;└── state/&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── queue.jsonl        # append-only queue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── claims/&amp;#x3C;host&gt;/     # lease via atomic mkdir (owner + epoch)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── findings.jsonl     # everything starts as &quot;candidate&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── dead-ends.md       # verified negatives + the control used&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ├── intel.md           # reusable patterns: &quot;[tag] fact → action&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    └── loot/F-0001/       # request.txt, response.txt&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;An attacker’s loop:&lt;/p&gt;
&lt;div class=&quot;code-frame&quot;&gt;&lt;div class=&quot;code-frame__bar&quot;&gt;&lt;span class=&quot;code-frame__lang&quot;&gt;BASH&lt;/span&gt;&lt;button type=&quot;button&quot; class=&quot;code-frame__copy&quot; hidden data-copy=&quot;&quot;&gt;COPY&lt;/button&gt;&lt;/div&gt;&lt;pre class=&quot;astro-code psx-night&quot; style=&quot;background-color:#0b0c0c;color:#e6e0d1; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6E0D1&quot;&gt;HOST&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;=$(&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;bin/hb&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; next&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; a1&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; exit&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt;     # atomic lease on the next host&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;bin/hb&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; inscope&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOST&quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; bin/hb&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; release&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOST&quot;&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; a1&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;;&lt;/span&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt; exit&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;grep&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -i&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOST&quot;&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; state/dead-ends.md&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; state/intel.md&lt;/span&gt;&lt;span style=&quot;color:#8A877E;font-style:italic&quot;&gt;   # don&apos;t repeat dead tests&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;ffuf&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -u&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;https://&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOST/FUZZ&quot;&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -w&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; wordlists/base.txt&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -ac&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -rate&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 20&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -t&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; 10&lt;/span&gt;&lt;span style=&quot;color:#F2C230&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;recon/&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOST/ffuf.json&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;bin/hb&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; finding&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; a1&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOST&quot;&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; idor&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;https://&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOST/api/orders?id=2&quot;&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;BOLA on /orders&quot;&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;200 with account B&apos;s order&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5FD3C6&quot;&gt;bin/hb&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; done&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;&lt;/span&gt;&lt;span style=&quot;color:#A39F93&quot;&gt;$&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt;HOST&quot;&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; a1&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; findings&lt;/span&gt;&lt;span style=&quot;color:#7BD36B&quot;&gt; &quot;1 candidate&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The guardrails are executable, not promises from the model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scope&lt;/strong&gt;: &lt;code&gt;hb inscope&lt;/code&gt; exits &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt; or &lt;code&gt;3&lt;/code&gt;; deny beats allow; unlisted hosts don’t get touched.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rate limits&lt;/strong&gt;: an explicit cap per tool, manual requests at 1/s per host, and a burst of 403/429 pauses the host for 10 minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proof before claims&lt;/strong&gt;: without request and response in &lt;code&gt;state/loot/&lt;/code&gt;, it’s only a candidate. Triage reproduces from scratch with a negative control, on a model from a different family.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Human review&lt;/strong&gt;: the agent can draft the report, but the manual reproduction, the review and the submission are mine.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That deserves a post of its own. Next time.&lt;/p&gt;
&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;a class=&quot;heading-anchor&quot; aria-hidden=&quot;true&quot; tabindex=&quot;-1&quot; href=&quot;#wrapping-up&quot;&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A harness won’t think for you, but it makes your method run the same way every time, in a terminal that’s nice to look at. Recap:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A terminal with a configurable font: Warp (macOS/Linux/Windows) or Windows Terminal.&lt;/li&gt;
&lt;li&gt;JetBrainsMono Nerd Font: &lt;code&gt;brew install --cask&lt;/code&gt; on macOS, zip + &lt;code&gt;fc-cache&lt;/code&gt; on Linux, zip → Install on Windows; selected in the terminal.&lt;/li&gt;
&lt;li&gt;(Optional) zsh + Oh My Zsh + Powerlevel10k, &lt;code&gt;p10k configure&lt;/code&gt; (on Windows, inside WSL2).&lt;/li&gt;
&lt;li&gt;omp: &lt;code&gt;brew install can1357/tap/omp&lt;/code&gt;, &lt;code&gt;curl -fsSL https://omp.sh/install | sh&lt;/code&gt; or &lt;code&gt;irm https://omp.sh/install.ps1 | iex&lt;/code&gt;; then &lt;code&gt;omp setup&lt;/code&gt; and &lt;code&gt;omp login&lt;/code&gt; or an API key.&lt;/li&gt;
&lt;li&gt;Theme and icons: &lt;code&gt;theme.dark&lt;/code&gt;, &lt;code&gt;symbolPreset&lt;/code&gt;, &lt;code&gt;statusLine.preset&lt;/code&gt;, &lt;code&gt;composer.shape&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Memory, secrets and approvals.&lt;/li&gt;
&lt;li&gt;Model roles and, if you need it, a gateway in &lt;code&gt;models.yml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/vibe&lt;/code&gt; once the basics are boringly reliable.&lt;/li&gt;
&lt;/ol&gt;</content:encoded><category>omp</category><category>ai-agents</category><category>tooling</category><category>terminal</category></item></channel></rss>