<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Konstantin Gredeskoul</title>
    <description>The latest articles on DEV Community by Konstantin Gredeskoul (@kigster).</description>
    <link>https://dev.to/kigster</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F662581%2F33b9dfad-3922-4dfd-ab2b-23085bd4b803.jpeg</url>
      <title>DEV Community: Konstantin Gredeskoul</title>
      <link>https://dev.to/kigster</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kigster"/>
    <language>en</language>
    <item>
      <title>Sopsy: Secrets in Git, Sealed by the Secure Enclave</title>
      <dc:creator>Konstantin Gredeskoul</dc:creator>
      <pubDate>Sat, 22 Aug 2026 00:31:16 +0000</pubDate>
      <link>https://dev.to/kigster/sopsy-secrets-in-git-sealed-by-the-secure-enclave-57f1</link>
      <guid>https://dev.to/kigster/sopsy-secrets-in-git-sealed-by-the-secure-enclave-57f1</guid>
      <description>&lt;p&gt;Well, I did a thing I've wanted to do for a while: I published &lt;strong&gt;&lt;a href="https://github.com/kigster/sopsy" rel="noopener noreferrer"&gt;sopsy&lt;/a&gt;&lt;/strong&gt; — my &lt;strong&gt;first open-source package written in Rust&lt;/strong&gt; — to &lt;a href="https://crates.io/crates/sopsy" rel="noopener noreferrer"&gt;crates.io&lt;/a&gt; and to Homebrew. After a couple of decades of shipping Ruby, some C, and an unreasonable amount of Bash, watching &lt;code&gt;cargo publish&lt;/code&gt; go green on a crate of my own felt genuinely great. I am allowing myself a small victory lap before Rust inevitably finds a new way to humble me.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;And like all the "cool kids" these days, of course it has its own snazzy website, because apparently a CLI is not real until it has a domain name. Visit &lt;a href="https://sopsy-cli.dev" rel="noopener noreferrer"&gt;sopsy-cli.dev&lt;/a&gt; now, or after you're done reading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is it, and why might you care?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;sopsy lets your team commit its secrets — encrypted — directly into git, where each developer holds their own private key inside their Mac's Secure Enclave, and decryption takes a fingerprint.&lt;/strong&gt; There's no shared master key (the single thing that ultimately sank my last attempt at this, more on that below). No secrets server to run or pay for. No plaintext &lt;code&gt;.env&lt;/code&gt; quietly making the rounds on Slack. Adding a teammate is a pull request; removing one re-keys every file automatically.&lt;/p&gt;

&lt;p&gt;Who is it for? Teams that want &lt;strong&gt;tight, revocable control over exactly who can decrypt the developer secrets they keep in git&lt;/strong&gt; — small-to-mid engineering teams, open-source projects, and security-conscious startups that would rather not stand up Vault just to stop passing &lt;code&gt;.env&lt;/code&gt; files around by hand. It is deliberately bottom-up: one developer installs it, tries it on a repo, and brings the rest of the team along. macOS-first for the hardware bits, today.&lt;/p&gt;

&lt;p&gt;The mental shift, compressed for the busy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A shared symmetric key (or a &lt;code&gt;.gitignore&lt;/code&gt;) asks you to &lt;strong&gt;guard one secret everyone copies.&lt;/strong&gt;&lt;br&gt;
sopsy lets each person &lt;strong&gt;hold their own key in hardware&lt;/strong&gt; — and re-keys the team when membership changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;First, the honest framing, because security tools get insufferable very quickly: &lt;strong&gt;sopsy does not reimplement any cryptography.&lt;/strong&gt; It stands on three excellent, boring, battle-tested tools that are kind of annoying to learn and use, and makes them, in my opinion, a lot more pleasant to live with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/getsops/sops" rel="noopener noreferrer"&gt;SOPS&lt;/a&gt;&lt;/strong&gt; — Mozilla's secrets operator, which cleverly encrypts the &lt;em&gt;values&lt;/em&gt; in a file while leaving the &lt;em&gt;keys&lt;/em&gt; readable, so a diff still tells you "&lt;code&gt;DATABASE_URL&lt;/code&gt; changed" or when the "&lt;code&gt;OPENAI_API_KEY&lt;/code&gt;" was added, without leaking what it changed to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/FiloSottile/age" rel="noopener noreferrer"&gt;age&lt;/a&gt;&lt;/strong&gt; — Filippo Valsorda's modern, no-options-to-get-wrong encryption, the spiritual antidote to GPG.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/remko/age-plugin-se" rel="noopener noreferrer"&gt;age-plugin-se&lt;/a&gt;&lt;/strong&gt; — the bit that made me warm and fuzzy: it generates an age identity whose &lt;strong&gt;private key is born inside your Mac's Secure Enclave and physically cannot leave it.&lt;/strong&gt; Decryption requires Touch ID. The key isn't on disk for malware to find, because it was never on disk at all. The fact that macOS has a &lt;a href="https://support.apple.com/guide/security/the-secure-enclave-sec59b0b31ff/web" rel="noopener noreferrer"&gt;Secure Enclave&lt;/a&gt; sitting there, quietly waiting to solve exactly this problem, was a huge surprise to me. But at the same time, it wasn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;sopsy is the friendly Rust CLI that orchestrates all three: it bootstraps a repo, manages who can decrypt, re-keys everything when membership changes, and ships a CI gate that fails the build the instant a plaintext secret sneaks in. SOPS stays the engine; sopsy is the steering wheel, the seatbelt, and the annoying little dashboard light that tells you when you are about to do something dumb.&lt;/p&gt;

&lt;p&gt;Here's the shape of it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpebi7vpqp1uallcnnj9f.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpebi7vpqp1uallcnnj9f.jpeg" alt="How Key Minting Works" width="800" height="1188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole trick is that arrow at the bottom. Encryption only needs your teammates' &lt;strong&gt;public&lt;/strong&gt; keys, so it's silent and offline. Decryption needs the &lt;strong&gt;private&lt;/strong&gt; key, which lives in silicon and answers only to your fingerprint. The ciphertext rides happily in git; the thing that unlocks it never does.&lt;/p&gt;

&lt;h3&gt;
  
  
  The five-minute tour
&lt;/h3&gt;

&lt;p&gt;You start a repo the way you'd hope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-app
sopsy init          &lt;span class="c"&gt;# checks your tools, mints a Secure Enclave identity, writes the config&lt;/span&gt;
sopsy edit .env.encrypted   &lt;span class="c"&gt;# opens your $EDITOR; decrypts in, re-encrypts on save (Touch ID)&lt;/span&gt;
sopsy check         &lt;span class="c"&gt;# the CI gate: fails if any plaintext secret is tracked &lt;/span&gt;
git add .sops.yaml .sopsy.yml .env.example .env.encrypted .gitignore
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Encrypted secrets, managed by sopsy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sopsy init&lt;/code&gt; prints your &lt;strong&gt;public recipient&lt;/strong&gt; — an &lt;code&gt;age1se1…&lt;/code&gt; string — and that's the only part of your identity that's ever shared or committed. For day-to-day use there's also &lt;code&gt;sopsy decrypt&lt;/code&gt; (and &lt;code&gt;sopsy secrets decrypt&lt;/code&gt;), which streams plaintext to stdout, so you can wire it into &lt;a href="https://direnv.net" rel="noopener noreferrer"&gt;direnv&lt;/a&gt; and unlock the whole environment once per shell instead of fighting it per-command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .envrc — one Touch ID when you cd in, then your secrets are just there&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sopsy decrypt .env.encrypted | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'/^#/d; /^$/d; s/^([A-Z])/export \1/g'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If Touch ID on every read gets old — you live in &lt;code&gt;direnv&lt;/code&gt;, or you run a dozen windows on one project — you can mint your Enclave key &lt;strong&gt;without&lt;/strong&gt; a biometric gate: &lt;code&gt;sopsy join "Your Name" --without-touch-id&lt;/code&gt;. The private key still never leaves the Enclave and is still device-bound; it just no longer prompts. This is a deliberate trade: less thumb, less ceremony.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Where it gets good: the team model
&lt;/h3&gt;

&lt;p&gt;A shared symmetric key has no notion of "who." sopsy does, and that's the whole reason it works for a team. Two files carry the state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.sops.yaml&lt;/code&gt;&lt;/strong&gt; — consumed by SOPS itself: a list of &lt;strong&gt;public&lt;/strong&gt; recipient keys that any new secret gets encrypted to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.sopsy.yml&lt;/code&gt;&lt;/strong&gt; — sopsy's own ledger: human names, who owns which key, lifecycle state, a small &lt;strong&gt;audit trail&lt;/strong&gt; (who requested access and when, who approved them and when), and the break-glass marker. A committed &lt;code&gt;.sopsy.sha&lt;/code&gt; checksum makes hand-edits tamper-evident instead of silently trusted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Onboarding is &lt;strong&gt;self-service&lt;/strong&gt;, which is the part that actually changes how a team feels. A newcomer doesn't chase anyone for a key; they mint their own and send the &lt;em&gt;public&lt;/em&gt; half in a pull request:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ooiseiehu23bbtg3l7q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ooiseiehu23bbtg3l7q.png" alt="Team Onboarding" width="273" height="1093"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note what a "pending" join grants: &lt;strong&gt;nothing.&lt;/strong&gt; The newcomer's key isn't in &lt;code&gt;.sops.yaml&lt;/code&gt; yet, so it can't decrypt anything — the request is just a public key sitting in a PR. (&lt;code&gt;sopsy join&lt;/code&gt; is also aliased &lt;code&gt;sopsy request-access&lt;/code&gt;, if that reads better in your team's docs.) An existing member reviews it, vouches that the key really belongs to Anna — you confirm that out-of-band, the same way you'd confirm a GPG fingerprint — and &lt;code&gt;sopsy approve&lt;/code&gt; does the rest: adds the key, re-wraps the data key for the new recipient set with &lt;code&gt;sops updatekeys&lt;/code&gt;, and flips Anna to active. Offboarding is the mirror image. No shared secret, no master password, no "we should probably rotate that someday."&lt;/p&gt;

&lt;p&gt;And because every developer's private key is hardware-bound, there are two deliberate safety valves for the cases where a fingerprint isn't available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Break-glass&lt;/strong&gt; — a portable age key you generate once, store offline in 1Password, and register as an emergency recipient. If everyone's laptop falls in the ocean, that one offline key can still open and re-key the repo. &lt;code&gt;sopsy&lt;/code&gt; nags you until you make one, and after this week I understand exactly why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI decryption&lt;/strong&gt; — &lt;code&gt;sopsy recipient ci&lt;/code&gt; runs the same guided ceremony to hand your pipeline a portable key: one CI secret named &lt;code&gt;SOPS_AGE_KEY&lt;/code&gt;, and your workflows can decrypt. The Secure Enclave, correctly, will never work headless — so this is how CI reads secrets without one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A little history: how I got here
&lt;/h2&gt;

&lt;p&gt;Nine years ago I wrote a little tool called &lt;a href="///2017/03/10/dead-simple-encryption-with-sym.html"&gt;sym&lt;/a&gt; and wrote a blog post about it.&lt;/p&gt;

&lt;p&gt;I opened the post with a complaint that has aged distressingly well: keeping your secrets &lt;em&gt;out&lt;/em&gt; of git, in a pile of &lt;code&gt;.gitignored&lt;/code&gt; files, gives you a &lt;strong&gt;false sense of security&lt;/strong&gt; and costs you everything good about version control. No history. No rollback. No diff. And the eternal team tax of "hey, can you AirDrop me the &lt;code&gt;.env&lt;/code&gt; again? mine's three weeks stale."&lt;/p&gt;

&lt;p&gt;The dream and ambition back then was not quite the same as today, but close: I just &lt;em&gt;didn't want to have plain text secrets lying around in my repos&lt;/em&gt;, even if they were git-ignored. Ideally, I wanted to encrypt them and check them in. Pre-commit hooks existed back then, so committing a plaintext secret to git was less of an issue. But "keys to many kingdoms" just lying around on my file system in plain text, waiting for someone to break in and steal them — that bothered me, annoyingly and continuously. So I wrote &lt;em&gt;sym&lt;/em&gt; — a symmetric encryption Ruby gem with a couple of macOS-specific twists. And for 2017 it wasn't so bad.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;sym&lt;/code&gt; is a Ruby gem that used OpenSSL encryption underneath, so that part was solid. You generated a symmetric key, and you could encrypt your secrets and commit them to your repo as one text blob (encrypted data was also base64 encoded). You had one master key in 1Password, and when you "onboarded" someone, you'd grab that master key, and they would import that key using &lt;code&gt;sym&lt;/code&gt; into OS X Keychain, in the process wrapping the key in an additional layer of password-protected encryption. Now you could git-commit the secrets, and to decrypt them on your machine required your password (whatever you assigned during the import). That was also a bonus. &lt;strong&gt;But a single master key was now the one thing that, if leaked, rendered the entire process completely pointless.&lt;/strong&gt; And in that sense, some security people would say things like &lt;em&gt;"Great tool, it gives you a warm and fuzzy feeling of protection, without actually providing it"...&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It had a few conveniences too — you could request that your password be cached in a local memcached instance, although there I got lazy and cached it in clear text. Security via obscurity, right?&lt;/p&gt;

&lt;p&gt;Still, it was way better than everyone sharing a plain text file with two dozen API keys that, if stolen, could have been used to rack up significant fees for the company, not to mention access some private data, which would have been way worse.&lt;/p&gt;

&lt;p&gt;So? That was fine for 2017, and a few projects I was on adopted it. It did not become a hit, because shortly thereafter Rails launched credentials, which, although it lacked the convenience of the password-protected and Keychain-stored encryption key, ultimately allowed you to keep encrypted credentials in the repo. With the popularity of Rails, there was no competition.&lt;/p&gt;

&lt;p&gt;Having recently been on a project where the &lt;code&gt;.env&lt;/code&gt; file contained nearly one hundred various secrets and API keys, I thought to myself, maybe we can do better in 2026?&lt;/p&gt;

&lt;h3&gt;
  
  
  The research
&lt;/h3&gt;

&lt;p&gt;After numerous conversations with Claude and ChatGPT about the best existing secrets-encryption tools for developers, and specifically — &lt;em&gt;in the development environment&lt;/em&gt; — I kept coming back to the same question.&lt;/p&gt;

&lt;p&gt;Production systems in the cloud have matured. If you're running on AWS, your keys are in KMS. If you're a Terraform nut, probably Vault. But locally, on your development MacBook, what is the preferred method to avoid keeping "keys to the kingdoms" lying around in plain text?&lt;/p&gt;

&lt;p&gt;Surely, the tool that encrypts your dev secrets and does not depend on a single symmetric key has been written by now? Well, maybe it has. But the three of us (the two AI agents and myself) could not find anything that did what I wanted, with the ease of use I had in mind.&lt;/p&gt;

&lt;p&gt;Having identified a potential hole in the developer tooling, and having collected information about the modern tools people use for encryption and key management, I concocted a plan. So over the last few days Claude and I built &lt;strong&gt;&lt;a href="https://github.com/kigster/sopsy" rel="noopener noreferrer"&gt;sopsy&lt;/a&gt;&lt;/strong&gt; — a Rust-based CLI for doing what I wanted in 2017, but could not quite build then without hand-waving around the most important part.&lt;/p&gt;

&lt;h2&gt;
  
  
  A parade of excellent bugs
&lt;/h2&gt;

&lt;p&gt;Now the fun part — and the honest part. I built sopsy over a handful of days pairing with an AI coding agent, and here's the detail I keep turning over: &lt;strong&gt;the agent wrote clean, well-tested code that passed its own suite green, and almost every real bug was found the moment a human actually &lt;em&gt;used&lt;/em&gt; the thing.&lt;/strong&gt; Not in CI. In my hands, on two real macOS accounts, with my actual thumb. That gap — between "the tests pass" and "it works for a person" — is the whole story of this section, and frankly the whole story of &lt;a href="///2026/06/22/writing-evals-for-ai-powered-apps.html"&gt;evals&lt;/a&gt; too. Determinism in the test suite is not correctness in the world.&lt;/p&gt;

&lt;p&gt;Here are the ones worth your time.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The key that wasn't there
&lt;/h3&gt;

&lt;p&gt;The first real run of &lt;code&gt;sopsy init&lt;/code&gt; ended with the most confusing class of failure: success, then nothing worked. It generated my Secure Enclave identity, encrypted the file, reported victory — and then every decrypt failed with &lt;code&gt;age: identity did not match any of the recipients&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cause is a beautiful little misunderstanding about how &lt;code&gt;age-plugin-se&lt;/code&gt; works. When it generates a key, it hands you back a long &lt;code&gt;AGE-PLUGIN-SE-1…&lt;/code&gt; string. It is tempting — and wrong — to treat that string as a throwaway, because "the private key is in the Enclave, so who cares." But that string is the &lt;strong&gt;handle&lt;/strong&gt;: the reference SOPS needs to ask the Enclave to unwrap anything. We were generating it and dropping it on the floor. The private key was safe in the Enclave, all right — and totally unaddressable, like a safe deposit box whose number you dropped in the toilet and accidentally flushed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The fix is the distinction worth internalizing: &lt;strong&gt;the handle is not the secret.&lt;/strong&gt; It only works on &lt;em&gt;this&lt;/em&gt; device, behind &lt;em&gt;this&lt;/em&gt; fingerprint, so it is perfectly safe to write to disk — and you &lt;em&gt;must&lt;/em&gt;, or the Enclave key is useless. &lt;code&gt;sopsy&lt;/code&gt; now persists it to the standard &lt;code&gt;sops&lt;/code&gt; keys file and points every SOPS call at it. The private key still never leaves the chip. The map to it just needs to exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. The &lt;code&gt;init&lt;/code&gt; that ran forever
&lt;/h3&gt;

&lt;p&gt;Early on, a teammate (me, in a different mood) ran &lt;code&gt;sopsy init&lt;/code&gt; in the wrong directory — one that wasn't a fresh repo, so git happily walked &lt;em&gt;up&lt;/em&gt; and decided the enclosing repository was my entire home folder. sopsy then tried to find every encrypted file to re-key it by... reading every file under the repo root. All of &lt;code&gt;$HOME&lt;/code&gt;. It was still going, fans spinning, several minutes and a few gigabytes of &lt;code&gt;node_modules&lt;/code&gt; later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An unbounded "scan everything" is a bug that hides until someone points it at a big enough directory, and then it's a hang, not an error — the worst kind. The fix was to stop walking the filesystem and instead expand the declared encrypted-file globs directly (a &lt;code&gt;*&lt;/code&gt; that never crosses a &lt;code&gt;/&lt;/code&gt;), so the scan touches the directories the patterns actually name and nothing else. And &lt;code&gt;init&lt;/code&gt; now refuses to silently adopt your home directory as a secrets repo — it stops and asks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. The secret that couldn't be committed (and then couldn't be shared)
&lt;/h3&gt;

&lt;p&gt;This one was a two-part trap, and it's my favorite because each half was invisible without the other.&lt;/p&gt;

&lt;p&gt;The default &lt;code&gt;.gitignore&lt;/code&gt; sopsy wrote was a touch too greedy. It ignored &lt;code&gt;.env.*&lt;/code&gt; to keep stray dotenv files out of git — sensible! — but &lt;code&gt;.env.example.encrypted&lt;/code&gt; matches &lt;code&gt;.env.*&lt;/code&gt; too. So the one file you &lt;em&gt;wanted&lt;/em&gt; committed was quietly untracked. That's part one.&lt;/p&gt;

&lt;p&gt;Part two: when a new member was approved, the re-key step enumerated files via &lt;code&gt;git ls-files&lt;/code&gt;. Reasonable, except that command deliberately skips ignored files — so the encrypted artifact that part one had just hidden was &lt;em&gt;also&lt;/em&gt; skipped during re-keying. The new member got added to &lt;code&gt;.sops.yaml&lt;/code&gt;, everything reported success, and then they couldn't decrypt one of the files, because they'd never been added to it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The lesson is a clean little invariant: &lt;strong&gt;encrypted artifacts are first-class committed files, full stop.&lt;/strong&gt; The gitignore now explicitly rescues every &lt;code&gt;*.encrypted&lt;/code&gt; no matter what, and re-keying enumerates secrets by globbing the working tree — not by asking git, which has opinions about what's worth mentioning. When two reasonable behaviors compose into one wrong behavior, the invariant was wrong or incomplete. That is where the bug was hiding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;sudo&lt;/code&gt; cannot touch the Enclave (and that's the point)
&lt;/h3&gt;

&lt;p&gt;I tested the two-person flow with two macOS accounts on one laptop, and switched between them with &lt;code&gt;sudo su - arc&lt;/code&gt;. Decryption failed every time, no Touch ID prompt, just the same flat "no identity matched." I was &lt;em&gt;certain&lt;/em&gt; this was a sopsy bug. It is not a sopsy bug.&lt;/p&gt;

&lt;p&gt;The Secure Enclave will only release a key to the user's &lt;strong&gt;active GUI login session.&lt;/strong&gt; &lt;code&gt;sudo&lt;/code&gt;, &lt;code&gt;su&lt;/code&gt;, SSH — none of them can raise the biometric prompt, so the Enclave just says no. The instant I used &lt;strong&gt;Fast User Switching&lt;/strong&gt; to actually log &lt;code&gt;arc&lt;/code&gt; in at the screen, the prompt appeared, my thumb did its thing, and the secret opened.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This isn't a wart, it's the &lt;strong&gt;threat model doing its job.&lt;/strong&gt; A key you can use over SSH is a key an attacker can use over SSH. Hardware-bound, presence-required decryption is exactly what you want for production secrets — and it's precisely why the portable break-glass and CI keys exist for the headless cases where the Enclave, correctly, won't play. Touch ID is per-account, too: each user enrolls their own fingerprint, falling back to that account's login password.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There were smaller ones — my own tests cheerfully writing fake keys into my &lt;em&gt;real&lt;/em&gt; keystore, a Homebrew install shadowing the freshly-built &lt;code&gt;cargo&lt;/code&gt; binary on &lt;code&gt;$PATH&lt;/code&gt; so I kept testing the old bits — but the four above are the ones that taught me something. Every single one survived a green test suite and died on contact with a human. Annoying? Yes. Useful? Also yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  sopsy in one glance
&lt;/h2&gt;

&lt;p&gt;The whole feature set, for the skimmers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-command bootstrap&lt;/strong&gt; — &lt;code&gt;sopsy init&lt;/code&gt; writes &lt;code&gt;.sops.yaml&lt;/code&gt;, &lt;code&gt;.env.example&lt;/code&gt;, an encrypted &lt;code&gt;.env.encrypted&lt;/code&gt;, &lt;code&gt;.gitignore&lt;/code&gt; safety rules, and &lt;code&gt;.sopsy.yml&lt;/code&gt;. Idempotent, so re-running is always safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Enclave identities&lt;/strong&gt; — your private key is generated inside Apple Silicon hardware, gated by Touch ID, and impossible to read or exfiltrate. &lt;code&gt;--without-touch-id&lt;/code&gt; if you want it device-bound but prompt-free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-service membership with an audit trail&lt;/strong&gt; — &lt;code&gt;sopsy join&lt;/code&gt; (alias &lt;code&gt;request-access&lt;/code&gt;) records a &lt;em&gt;pending&lt;/em&gt; request; any active member &lt;code&gt;sopsy approve&lt;/code&gt;s. &lt;code&gt;.sopsy.yml&lt;/code&gt; keeps who asked, their username, when, and who approved them and when; &lt;code&gt;sopsy recipient list&lt;/code&gt; prints it all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic re-keying&lt;/strong&gt; — every add / remove / approve re-wraps all secrets via &lt;code&gt;sops updatekeys&lt;/code&gt;, so the recipient set never drifts from the ciphertext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Friendly edits + scriptable crypto&lt;/strong&gt; — &lt;code&gt;sopsy edit&lt;/code&gt; opens an encrypted file in your &lt;code&gt;$EDITOR&lt;/code&gt; with auto file-type detection; &lt;code&gt;sopsy encrypt&lt;/code&gt; / &lt;code&gt;sopsy decrypt&lt;/code&gt; (and the &lt;code&gt;secrets&lt;/code&gt; forms) do one-shot, &lt;code&gt;direnv&lt;/code&gt;-friendly crypto to stdout or a file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break-glass + CI ceremonies&lt;/strong&gt; — &lt;code&gt;sopsy recipient break-glass&lt;/code&gt; mints an offline emergency key; &lt;code&gt;sopsy recipient ci&lt;/code&gt; mints a portable CI decryption key (one &lt;code&gt;SOPS_AGE_KEY&lt;/code&gt; secret and your pipeline can decrypt).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tamper-evident config&lt;/strong&gt; — a committed &lt;code&gt;.sopsy.sha&lt;/code&gt; checksum over &lt;code&gt;.sopsy.yml&lt;/code&gt; + the admin key is refreshed on write and verified on read, so hand-edits are surfaced, not silently trusted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A CI gate that bites&lt;/strong&gt; — &lt;code&gt;sopsy check&lt;/code&gt; enforces hygiene invariants and exits non-zero on any tracked plaintext. It needs no key, so it runs on a Linux runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sopsy doctor&lt;/code&gt;&lt;/strong&gt; — a grouped, colorful health report of your tools, Secure Enclave, Touch ID, and repo that you can paste straight into a GitHub issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;If you keep secrets in git today — or you've been getting away with a shared &lt;code&gt;.env&lt;/code&gt; and quietly worrying about it — I'd genuinely love to hear how your team handles this. What are you using? What would stop you from committing encrypted secrets right next to the code? Install it (&lt;code&gt;brew install kigster/tap/sopsy&lt;/code&gt; or &lt;code&gt;cargo install sopsy&lt;/code&gt;), point it at a throwaway repo, and tell me what breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leave a comment below&lt;/strong&gt; — bug reports, "you reinvented X," and "why not just use Y" are all equally welcome. And if it's useful to you, a star on &lt;a href="https://github.com/kigster/sopsy" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; helps more people find it.&lt;/p&gt;

&lt;p&gt;Go encrypt something. Then try to commit it. That part is supposed to feel good now.&lt;/p&gt;

&lt;p&gt;— Konstantin Gredeskoul&lt;/p&gt;

&lt;p&gt;San Francisco, CA, June 28, 2026.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In the spirit of full disclosure&lt;/strong&gt;: sopsy was written over a few days pairing with an AI coding agent, and the experience is the quiet subtext of this whole post. The agent was a frankly excellent programmer — clean Rust, real tests, all green. And it could not, on its own, find a single one of the bugs above, because every one of them required a human, a thumb, two login sessions, and a fair bit of swearing. The code was AI-built; the &lt;em&gt;correctness&lt;/em&gt; and &lt;em&gt;validation&lt;/em&gt; came from me yelling at Claude, compacting the context more than once, and generally being pretty annoyed with it. Keep a human in the loop, especially the one holding the finger and the fingerprint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kigster/sopsy" rel="noopener noreferrer"&gt;sopsy on GitHub&lt;/a&gt; — source, install instructions, and the owner/member guides.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://crates.io/crates/sopsy" rel="noopener noreferrer"&gt;sopsy on Crates.io&lt;/a&gt; — where you'd install it from.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/getsops/sops" rel="noopener noreferrer"&gt;SOPS&lt;/a&gt; · &lt;a href="https://github.com/FiloSottile/age" rel="noopener noreferrer"&gt;age&lt;/a&gt; · &lt;a href="https://github.com/remko/age-plugin-se" rel="noopener noreferrer"&gt;age-plugin-se&lt;/a&gt; — the three tools sopsy stands on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="///2017/03/10/dead-simple-encryption-with-sym.html"&gt;Dead Simple Encryption with Sym&lt;/a&gt; — the 2016-era ancestor, and the problem statement that still holds.&lt;/li&gt;
&lt;li&gt;
&lt;a href="///2026/06/22/writing-evals-for-ai-powered-apps.html"&gt;Evals: The Unit Tests for the Non-Deterministic Parts of Your App&lt;/a&gt; — more on the gap between "tests pass" and "it works."&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>security</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>What 25 Years of Deterministic Software Engineering Taught Me About Building AI Systems</title>
      <dc:creator>Konstantin Gredeskoul</dc:creator>
      <pubDate>Tue, 23 Jun 2026 16:49:46 +0000</pubDate>
      <link>https://dev.to/kigster/what-25-years-of-deterministic-software-engineering-taught-me-about-building-ai-systems-36oa</link>
      <guid>https://dev.to/kigster/what-25-years-of-deterministic-software-engineering-taught-me-about-building-ai-systems-36oa</guid>
      <description>&lt;p&gt;One of the strangest things about AI engineering is that your test suite can be 100% green while your product is getting worse.&lt;/p&gt;

&lt;p&gt;Traditional software taught us to think in absolutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pass/fail&lt;/li&gt;
&lt;li&gt;correct/incorrect&lt;/li&gt;
&lt;li&gt;deterministic/non-deterministic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI systems force us to think in distributions, thresholds, confidence intervals, and trade-offs.&lt;/p&gt;

&lt;p&gt;It’s a subtle shift, but it changes how you build, test, and deploy software.&lt;/p&gt;

&lt;p&gt;I collected some of the lessons I’ve learned while building AI-powered applications and learning evals.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kig.re/2026/06/22/writing-evals-for-ai-powered-apps.html" rel="noopener noreferrer"&gt;https://kig.re/2026/06/22/writing-evals-for-ai-powered-apps.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curious how others are approaching evaluation and regression testing in production AI systems.&lt;/p&gt;

&lt;p&gt;And here is the summary of most recent research on the subject so you don't have to :-)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kig.re/2026/06/22/ai-digest-on-evals.html" rel="noopener noreferrer"&gt;https://kig.re/2026/06/22/ai-digest-on-evals.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>evals</category>
      <category>appliedai</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
