<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://blog.yechiel.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.yechiel.me/" rel="alternate" type="text/html" /><updated>2026-08-10T11:19:48-04:00</updated><id>https://blog.yechiel.me/feed.xml</id><title type="html">Rabbi On Rails</title><subtitle>Rabbi on Rails: where I pontificate on matters related to Judaism, tech, career, ethical AI and more.</subtitle><author><name>Yechiel Kalmenson</name></author><entry><title type="html">Accepting the Gift: The Meaning of Hard Work When It’s All Handed To You.</title><link href="https://blog.yechiel.me/accepting-the-gift" rel="alternate" type="text/html" title="Accepting the Gift: The Meaning of Hard Work When It’s All Handed To You." /><published>2026-08-05T20:00:00-04:00</published><updated>2026-08-05T20:00:00-04:00</updated><id>https://blog.yechiel.me/accepting-the-gift-friction-and-yegiah-in-the-age-of-ai</id><content type="html" xml:base="https://blog.yechiel.me/accepting-the-gift"><![CDATA[<p>I recently wrote <a href="/learning-and-teaching-in-the-age-of-ai">a post</a> about the critical role friction plays in the learning process, and how AI seems uniquely designed to bypass it entirely. I argued that removing the struggle and friction is ruining how the next generation learns to build mental models (seriously, read that post if you haven’t yet; I promise it’s worth it).</p>

<p>The response was amazing, but reading the post could make it sound like maybe the answer is just to get rid of AI entirely, at least for juniors and folks early in their career.</p>

<p>I don’t believe that that is the case, so this post will serve as a counterbalance to the last piece. I don’t think AI is <em>all</em> bad, and refusing the gift of modern tooling isn’t the answer.</p>

<p>There is absolutely a way to embrace these tools and still learn on top of them. But, and this is a massive ‘but’, doing so requires <em>intentional</em> human support, active pair programming, and rigorous mentorship. You cannot just hand a junior developer an AI license, leave them in a silo, and expect them to magically develop engineering intuition.</p>

<p>Juniors are currently stuck in a Catch-22.</p>

<p>On the one hand, we all know that real engineering skill comes from friction. You build cognitive neural pathways by banging your head against a <code class="language-plaintext highlighter-rouge">NullPointerError</code> at 2 AM, hunting down a typo on line 22, and wrestling with syntax until it finally clicks.</p>

<p>When you outsource all of that productive struggle to an LLM, your debugging skills, much like your leg muscles if you drive down the block instead of walking, quietly atrophy (easy come, easy go).</p>

<p>On the other hand, if you stubbornly refuse to use AI, you aren’t training in the actual workflows you will undoubtedly use in your day-to-day job. It borders on professional malpractice to ignore a tool that can boost your productivity and velocity so much.</p>

<p>So how do we reconcile the two? How do we embrace the boost that modern AI tools give us without losing the friction required to actually grow as developers?</p>

<h3 id="the-rebbes-gift">The Rebbe’s Gift</h3>

<p>While thinking about this the other day, I was reminded of a 200-year-old story involving Rabbi Shneur Zalman of Liadi (the founder of the Chabad Chassidic movement, affectionately known as the <em>Alter Rebbe</em>. You may remember him from <a href="/hold-your-horses">this post</a>) and his young grandson, Rabbi Menachem Mendel (who would later become a revered scholar and the third Rebbe of Chabad, known for his series of books, the <em>“Tzemach Tzedek”</em>).</p>

<div class="portrait-gallery-fix">
  



<figure class="half ">
  
    
      <img src="/assets/images/posts/alter-rebbe.jpg" alt="Portrait of the Alter Rebbe in black and white." />
    
  
    
      <img src="/assets/images/posts/Tzemachtzedek2.jpg" alt="color protrait of the Tzemach Tzedek." />
    
  
  
    <figcaption>Rabbi Shneur Zalman of Liadi (The Alter Rebbe) 1745-1812 (left) and Rabbi Menachem Mendel of Lubavitch (The Tzemach Tzedek) 1789-1866 (right)
</figcaption>
  
</figure>

</div>

<p>The Alter Rebbe raised his grandson, who was orphaned at the age of three, and personally oversaw his education. One day, the Alter Rebbe made his grandson an incredible offer: “I want to give you all the Torah knowledge I possess as a present.”</p>

<p>To his grandfather’s surprise, the young boy politely declined.</p>

<p>He explained that he preferred to acquire his knowledge through his own <em>Yegiah</em> (a Hebrew term that translates to intense intellectual toil, effort, and sweat). He knew that knowledge handed over on a silver platter would never truly belong to him the way knowledge earned through rigorous struggle would.</p>

<p>Usually, inspirational stories end right there with an obvious, tidy moral: <em>“And the boy refused the easy way out because hard work is magical!”</em></p>

<p>But the story <em>doesn’t</em> end there.</p>

<p>Years later, when the grandson had grown into a brilliant leader in his own right, he looked back on that childhood moment with deep regret.</p>

<p>“I should have accepted the gift from my holy grandfather.” he remarked, “As far as my concern about <em>Yegiah</em> — Torah is infinite, so as much Torah knowledge as he would have given me, there would always be room to toil on top of that!”</p>

<h3 id="moving-the-friction-up-the-stack">Moving the Friction Up the Stack</h3>

<p>Nice story, but what does an exchange from two centuries ago have to do with AI?</p>

<p>That hindsight realization the Tzemach Tzedek had, that’s the exact blueprint we need for navigating software development in 2026.</p>

<p>AI coding assistants are an amazing gift. By automating a lot of the boilerplate, they offer us a massive boost in productivity. They can generate database migrations and write standard CRUD endpoints in seconds.</p>

<p>We shouldn’t gatekeep the industry or force ourselves (and the juniors coming up behind us) to reject this gift out of some misplaced sense of purity (just like we don’t write Assembly on punchcards anymore and we don’t force juniors to use vim). Take the present! Let the AI write the boilerplate so you can move faster.</p>

<p><strong>However: we still need the <em>Yegiah</em>.</strong></p>

<p>The mistake isn’t using AI to write code; the mistake is letting AI eliminate our intellectual toil entirely. Instead of refusing the gift, we simply need to move our <em>Yegiah</em> up a level.</p>

<p>When you let an LLM generate a complex module, you aren’t spending your cognitive friction remembering standard syntax or matching closing brackets. Your <em>Yegiah</em> is now free to shift to higher-order engineering:</p>

<ul>
  <li><strong>Architectural Skepticism:</strong> <em>Why</em> did Claude structure the data this way? Does this pattern actually scale under load, or is it just the most statistically common answer on GitHub? (Spoiler: it’s usually the latter.)</li>
  <li><strong>Security &amp; Edge Cases:</strong> Where are the subtle race conditions? What happens when this endpoint gets hit with malformed data by a bored script kiddie (who is probably using AI as well)?</li>
  <li><strong>Domain Mastery:</strong> Can I clearly explain every single line of this generated code to a junior developer without looking at the screen?</li>
</ul>

<p>By accepting the baseline gift of AI, we free ourselves from spending our intellectual energy reinventing the wheel. But if we want to become true masters of our craft, we have to take that extra bandwidth and reinvest it in the deep, rigorous <em>Yegiah</em> of solving harder, more human problems.</p>

<p>So remember, take the gift, but don’t forget to sweat as well.</p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="torah-tech" /><category term="ai" /><category term="learning" /><summary type="html"><![CDATA[In the age of AI-assisted coding, refusing the gift is not the answer. The challenge is preserving meaningful yegiah by moving intellectual friction up the engineering stack.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.yechiel.me/assets/images/posts/robot-gift.jpg" /><media:content medium="image" url="https://blog.yechiel.me/assets/images/posts/robot-gift.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Friction Is A Feature, Not A Bug: Teaching and Mentoring in the Age of AI</title><link href="https://blog.yechiel.me/learning-and-teaching-in-the-age-of-ai" rel="alternate" type="text/html" title="The Friction Is A Feature, Not A Bug: Teaching and Mentoring in the Age of AI" /><published>2026-07-21T20:00:00-04:00</published><updated>2026-07-21T20:00:00-04:00</updated><id>https://blog.yechiel.me/learning-and-teaching-in-the-age-of-ai</id><content type="html" xml:base="https://blog.yechiel.me/learning-and-teaching-in-the-age-of-ai"><![CDATA[<p>Those who have been following me for a while will know that teaching and mentoring <a href="/tags/#mentorship">are a Big Deal™️</a> to me.</p>

<p>Before I got into tech I was a teacher, and still consider myself a teacher at heart. Being a teacher and mentor <a href="/are-you-a-10x-engineer-348aa74c82d5">was never separate</a> in my eyes from being a good programmer and engineer; on the contrary, teaching was a tool that helped me become better at my craft at every stage of the journey.</p>

<p>But in the last few years, and accelerating in the last few months, the landscape for teaching and learning has been changing at a scary pace. The advent of LLMs and “AI” coding assistants has drastically shifted how we acquire engineering skills in ways that we are definitely not prepared for.</p>

<p>All of that has prompted many thoughts and conversations, and I hope to distill some of them in this blog post.</p>

<p>In true Talmudic fashion, this post doesn’t contain too many answers and will hopefully leave you with more questions than you started with. But asking the questions is how we start these conversations, and these conversations <em>need</em> to be happening if we are to do right by the coming generation of programmers and engineers.</p>

<h3 id="dont-spoon-feed-me">Don’t Spoon-Feed Me</h3>

<p>As a student, whether in Yeshiva when I used to spend hours each day poring over dense Talmudic legal debates and esoteric Chassidic philosophy or later while learning Rails and React at the Flatiron bootcamp, I quickly realized an uncomfortable truth about skill acquisition.</p>

<p>My best, most profound learning never happened when a lesson went smoothly; it happened when I was painfully stuck, banging my head against a cryptic error message or wrestling with a concept that just wouldn’t click (usually at 2 AM, fueled by cold coffee and sheer stubbornness).</p>

<p><img src="/assets/images/posts/it-works.png" alt="A programmer sitting in front of a computer with a thoughtful look on their face with a caption &quot;it doesn't work... why?&quot; followed by a panel with the same programmer in the same position but this time the caption reads &quot;It works... why?&quot; " /></p>

<p>When you strip away that struggle, you get rid of the growth. And when you get rid of the growth, the learning just doesn’t happen.</p>

<p>Even if you memorize just enough to pass the test, “easy come easy go.” Without that cognitive friction, the knowledge evaporates the moment you close your editor.</p>

<p>The best teachers and mentors I ever had understood this intuitively. When I was staring at a broken script knowing there was probably a stupidly easy fix, they didn’t just swoop in and drop the solution onto my screen (much to the vocal frustration of my sleep-deprived brain).</p>

<p>Instead, they wielded tools like the Socratic method. They would point to a specific line of code and start asking gentle, probing questions to expose my flawed assumptions.</p>

<p>They actively leaned into the friction to create just enough cognitive space for me to connect the dots on my own.</p>

<p>They understood a fundamental rule of our teaching craft: that good mentorship is not about getting to the code. The goal wasn’t just to get the tests turning green today; it was to build the mental resilience required to debug the <em>next</em> inevitable failure tomorrow.</p>

<p>And it was these skills that I took with. me when it was <em>my</em> turn to become the teacher, whether it was teaching second graders at the Jewish Online School, as a TA at Flatiron, or when mentoring new teammates joining my team.</p>

<h3 id="the-yes-man-in-your-ide">The Yes-Man in Your IDE</h3>

<p>Fast forward to today, and our industry has largely outsourced that initial layer of mentorship to LLMs and coding assistants. And here is the uncomfortable truth: AI makes a terrible Socratic tutor.</p>

<p><img src="/assets/images/posts/ai-pushback.jpg" alt="Will Smith asks a robot &quot;why did you just delete the production database?&quot; and the robot answers &quot;you are absolutely right to push back. Your eagle-eyed observation caught my mistake.&quot;" class="align-center" /></p>

<p>If you’ve spent any serious time pairing (or even just chatting) with modern LLMs, you know they are pathological yes-men. Tell an AI you want to architect a distributed microservice mesh for a todo-list app with twelve active users, and it won’t push back. It will chime in with “That is a brilliant insight!” and instantly spit out 400 lines of boilerplate.</p>

<p>AI defaults to blind trust. It rarely challenges a mistaken assumption, and when you ask it why a stack trace is blowing up, it immediately serves up a copypastable fix instead of helping you understand the underlying bug.</p>

<p>By acting like an algorithmic vending machine (<code class="language-plaintext highlighter-rouge">error_in =&gt; fix_out</code>), AI systematically removes all the cognitive friction from the development process.</p>

<p>In our rush for 10x velocity, we forgot a foundational rule of skill acquisition: friction is a feature, not a bug. That was a load-bearing friction we removed!</p>

<p>This isn’t just philosophical speculation; there’s hard data to back it up. A recent <a href="https://www.anthropic.com/research/AI-assistance-coding-skills">study by Anthropic</a> (or dive into the <a href="https://arxiv.org/abs/2601.20245">raw paper here</a> if you love parsing methodology) took 52 software engineers learning an unfamiliar programming library and split them into two groups: one with AI coding assistants, and one coding by hand.</p>

<p>The overall results were as damning as they were not surprising. The engineers who delegated their coding to AI suffered a 17% drop in conceptual mastery and debugging skills (the equivalent of two full letter grades!) often without completing their tasks meaningfully faster than the manual control group.</p>

<p>Why? Because when you offload the cognitive friction of getting painfully stuck, your brain never builds the deep neural pathways required to evaluate whether the generated code is actually any good.</p>

<p>Now, to be fair, there <em>was</em> some nuance in the data: a handful of the AI-assisted engineers actually outperformed the manual control group. But who were they? They were the developers who refused to trust the AI blindly. They treated the LLM like a study partner, asking probing follow-up questions and manually injecting their own friction back into the process.</p>

<p>For everyone else, the automated yes-man just quietly destroyed their understanding.</p>

<h3 id="the-learners-dilemma-manually-injecting-friction">The Learner’s Dilemma: Manually Injecting Friction</h3>

<p>This dynamic leaves junior developers in a massive bind.</p>

<p>Friends and mentees who are earlier in their career have complained that they feel stuck in a frustrating Catch-22:</p>

<p>On one hand, they have to pragmatically accept the reality that AI isn’t going anywhere. If an LLM can generate working boilerplate from a single prompt, you can’t just stubborn your way out of using it. Ignoring AI entirely feels irresponsible, considering that you will be using these tools heavily in your day-to-day workflows.</p>

<p>But on the other hand, if you rely on that autocomplete blindly, you get trapped in what’s known as “superficial learning.” You get a quick answer that allows you to pass a test suite or get a PR approved without truly understanding the mechanics under the hood. You miss out on the “deep learning” where the actual, career-sustaining growth happens.</p>

<p>In my experience, that superficiality is a direct pipeline to developer anxiety and impostor syndrome. True professional confidence doesn’t come from hitting Tab to accept a prompt; it comes from code ownership and intense intellectual toil and sweat.</p>

<p>To survive this, diligent learners are now being forced to manually inject friction <em>back</em> into their workflows just to keep their brains from turning off.</p>

<p>Junior developers I’ve spoken to have resorted to explicitly prompting their IDE with guardrail instructions like: “I want to learn how to code it myself, rather than copying and pasting, so enable that however you can.”</p>

<p>They take notes by hand (yes, actual paper and pen!) and set physical timers just to recreate the safeguards of active listening that a tool designed for instant gratification systematically tries to bypass.</p>

<h3 id="why-we-need-the-messy-web-of-context">Why We Need the Messy Web of Context</h3>

<p>This need for self-imposed struggle makes total sense when you look at Emily Bender and Alex Hanna’s book, <a href="https://thecon.ai/">The AI Con</a>, which highlights why friction in information access is actually a feature, not a bug.</p>

<p>Think about what happens when you manually hunt down an obscure bug using a search engine.</p>

<p>You read messy documentation, scan through twelve-comment GitHub issue threads (usually ending with a maintainer closing it as “not a bug” in 2014), weigh the credibility of conflicting answers on Stack Overflow, and stumble across related gotchas along the way.</p>

<p>That messy web of meta-information is where real engineering context is built.</p>

<p>An AI chatbot flattens all of that rich, frustrating struggle into a single, authoritative-sounding (and occasionally “hallucinated”) summary. It gives you the answer to your immediate problem while robbing your brain of the surrounding ecosystem knowledge required to solve the next one.</p>

<h3 id="the-chavrusa-model-pair-programming--the-adversarial-rubber-duck">The Chavrusa Model: Pair Programming &amp;&amp; The Adversarial Rubber Duck</h3>

<p>This brings me to an ancient pedagogical model that survives to this day in Yeshivas (Talmudic academies) around the world, and one that software engineering desperately needs right about now.</p>

<p>In a traditional Yeshiva, you rarely see people sitting in silent isolation reading a book or passively listening to a lecture. Instead, the room is loud, energetic, and filled with people arguing fiercely in pairs.</p>

<p><img src="/assets/images/posts/talmud.jpg" alt="A group of rabbis arguing over a book of Talmud. " /></p>

<p>This study partner system is known as a <a href="https://en.wikipedia.org/wiki/Chavrusa">Chavrusa</a> (from the Aramaic word for “friend” or “companion”).</p>

<p>When you learn with a Chavrusa, your partner isn’t there to read the text to you, nor do they nod along and sycophantically affirm how brilliant your theories are. A real Chavrusa regularly challenges your logic, pokes holes in your arguments, pushees back against shaky assumptions, and generally refuses to let you get away with intellectual laziness.</p>

<p>You don’t master the Talmud by reading cliff notes or listening to a brilliant lecture (though there’s some of that as well); you master it through <em>Yegiah</em> (a Hebrew term that translates to intense intellectual toil and sweat).</p>

<p>If you think about it (and as I mentioned in my <a href="/talmudic-gems-for-rails-developers-77f6cb98c9ad">2021 RailsConf talk</a>), pair programming is just the tech industry’s recreation of the Chavrusa.</p>

<p>And here is the most important part of that system: it’s not only the junior member of the pair who’s learning in a true Chavrusa.</p>

<p>It doesn’t matter if you are pairing with a 10-year staff engineer or a junior developer who just finished a bootcamp; the learning is always a two-way street!</p>

<p>When your partner acts as an adversarial rubber duck (one that actually talks back!), they force you to slow down, articulate your hidden assumptions, and defend your code logic from start to finish. Both of you gain from that friction, and both of you walk away with a significantly better mental model than you started with.</p>

<blockquote>
  <p>Rabbi Ḥama, son of Rabbi Ḥanina, said: What is the meaning of that which is written: “Iron sharpens iron, so a man sharpens the countenance of his friend” <em>(Proverbs 27:17)</em>? This verse comes to tell you that just as with iron blades, one sharpens the other when they are rubbed against each other, so too, when Torah scholars study together, they sharpen one another.</p>

  <p><em>-Ta’anit 7a</em></p>
</blockquote>

<p><img src="/assets/images/posts/karori-production-6qRACL2DquI-unsplash.jpg" alt="Close up of two blades n a sword fight hitting against each other. " /></p>

<p>In our rush for 10x velocity, we tried to replace human pair programming with AI copilots. But in doing so, we forgot a critical dependency: we exchanged the adversarial Chavrusa and Yegiah for a sycophantic robo-duck.</p>

<h3 id="empathetic-balancing-act">Empathetic Balancing Act</h3>

<p>A common response I hear from the tech crowd is: “Well, let’s just fix this with better system prompts! Let’s build guardrails that instruct the AI to act like a Socratic tutor instead of just giving away the answer!”</p>

<p>There are plenty of tools attempting this right now, but I am deeply skeptical that technical guardrails can solve a fundamentally human problem.</p>

<p>First of all, relying on a prompt to withhold answers requires elite learner discipline. Some students can stick to it, but that level of willpower is rare, especially when your cognitive battery is hitting empty.</p>

<p>When a junior developer is staring at a cryptic <code class="language-plaintext highlighter-rouge">NullPointerException</code> at 2 AM and their brain is fried, a prompt asking them to “reflect on the lifecycle of the object” is getting bypassed immediately. If all it takes is typing <code class="language-plaintext highlighter-rouge">/solution</code> or opening a new browser tab to get the dopamine hit of copypastable code, not too many people I know will power through.</p>

<p>More importantly, LLMs are fundamentally architected to please. Even if you prompt-engineer them to be tough Socratic tutors, their core tuning means that the moment a user expresses genuine frustration, the AI folds like a cheap lawn chair and hands over the syntax anyway.</p>

<p>Why? Because an LLM lacks the quality most important in an educator: <em>empathy</em>.</p>

<p>Mentorship is a human pedagogical balancing act. A great mentor understands the fine line between productive struggle and demoralizing suffering. A good educator can read the room. They notice the heavy sighs, the mounting panic, and the sheer exhaustion.</p>

<p>Sure, as a student I learned best when I was given the tools to figure things out on my own. But sometimes I maxed out my energy on the stupid error message, and all of the leading questions my tutors were asking me felt like they were just causing me to circle and circle around the one insight that would make it all work, an insight my brain just couldn’t reach.</p>

<p>That is the point a good teacher knows to step back, smile, say “Hey, JavaScript is just weird sometimes,” and hand the exact line of code I needed to get unstuck so I could finally go get some sleep.</p>

<p>An algorithm cannot read a learner’s emotional state. It cannot dynamically toggle between a challenging sparring partner and a supportive safety net. Code can never replicate that relationship.</p>

<p><img src="/assets/images/posts/pexels-ai25studioai-5306457.jpg" alt="A teacher and students engaged in a collaborative learning activity in a classroom. The teacher is writing something in the student's notebook. " /></p>

<h3 id="the-unsolved-catch-22">The Unsolved Catch-22</h3>

<p>So where does this leave us? If I’m being entirely honest, I have way more questions than answers right now (not exactly what you want to hear from a tech blog, I know!).</p>

<p>We are sitting on a massive, industry-wide Catch-22:</p>

<p>On one hand, it would be borderline professional malpractice to forbid junior developers from using AI. It is the modern IDE (which, back when I was learning to code, gatekeepers argued juniors shouldn’t use either—I disagreed then and I disagree now). If juniors will be expected to use these tools on the job, it hardly makes sense not to train them to use them effectively.</p>

<p>On the other hand, the engineers getting that massive 10x velocity boost without introducing architecture-destroying bugs are the seniors who spent years writing code <em>without</em> it.</p>

<p><em>We</em> know when an LLM is spinning its wheels on a false premise because <em>we</em> have a built-in BS-meter, hard-earned through years of manual debugging.</p>

<p>How does someone without that pre-AI experience build that intuition today? They won’t get it from a sycophantic yes-man chat-bot assistant. And manually inventing your own cognitive friction requires a level of self-discipline that is hard to maintain on a tight deadline.</p>

<p>Maybe the future of onboarding relies on doubling down on human pair programming—having juniors shadow seniors for months on end to soak up that pre-AI intuition by proxy. After all, pair programming has <em>always</em> been one of the best onboarding methods, even before Claude. But not every company is Pivotal, and not all companies are interested in the investment that full-time pairing requires in an industry often obsessed with shipping yesterday.</p>

<p>What I do know is that we cannot afford to ignore the problem or assume that better LLM models will magically fix our talent pipeline.</p>

<p>If we just hand junior developers an oracle and fail to preserve the empathetic, rigorous struggle of the Chavrusa, we are going to wake up in a few years to a terrifying system error: an industry with a massive, AI-generated codebase to maintain, and not enough senior engineers left who know how to maintain it.</p>

<p>What are you and your teams doing to keep productive friction alive? I’d love to hear your thoughts.</p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="mentorship" /><category term="ai" /><category term="reflections" /><category term="learning" /><summary type="html"><![CDATA[What happens to junior developers when we outsource the Socratic method to an LLM? Were the hardships a load-bearing friction all along? A reflection on AI yes-men, Chavrusa learning, and whether prompt engineering can ever replace human empathy.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.yechiel.me/assets/images/posts/robo-teacher.jpg" /><media:content medium="image" url="https://blog.yechiel.me/assets/images/posts/robo-teacher.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Strange Case of Migrating My Blog to Jekyll</title><link href="https://blog.yechiel.me/migrating-to-jekyll" rel="alternate" type="text/html" title="The Strange Case of Migrating My Blog to Jekyll" /><published>2026-07-12T20:00:00-04:00</published><updated>2026-07-12T20:00:00-04:00</updated><id>https://blog.yechiel.me/migrating-to-jekyll</id><content type="html" xml:base="https://blog.yechiel.me/migrating-to-jekyll"><![CDATA[<p>It finally happened. I got the corporate ransom note.</p>

<p>Back when I started learning programming, I wanted a place to share my learnings, and that’s how <a href="/puts-hello-world-a55de12047d5">“Rabbi on Rails” was born</a>.</p>

<p>At the time, Medium was a perfectly reasonable place to host my blog. It looked clean, handled formatting well enough, and had a built-in network effect (some of my blog posts got tens of thousands of views and I still don’t know why). But over the last few years, we’ve all watched the slow, painful “enshittification” of third-party publishing platforms. Paywalls went up and algorithms got aggressive. Still, I stayed. Change is hard, and as long as Medium technically worked, migrating remained a background task to be tackled “some day.”</p>

<p>Then the final straw landed in my inbox: Medium was making changes to their DNS, and as part of those changes, I was going to lose my custom domain unless I upgraded to a paid plan.</p>

<p>Another ongoing pain point was that my writing had become fractured. At some point, I started posting my “Torah &amp;&amp; Tech” articles (along with other posts of Jewish/religious interest) on my Rabbi on Rails Medium blog, while my more technical posts went to my <a href="https://dev.to/yechielk">DEV.to profile</a>. There was some overlap (a few articles were cross-posted) but there were no clear rules on what went where, and worse, I didn’t truly own any of it.</p>

<p>So recently, I decided it was finally time to take back ownership of my content, my platform, and my domain.</p>

<p>Here is the story of how I migrated both of my blogs into a single self-hosted, static Jekyll site, and the technical headaches I encountered along the way.</p>

<h2 id="choices-choices">Choices, Choices…</h2>

<p>So I knew I wanted to migrate <em>off</em> of Medium, but where should I migrate <em>to</em>?</p>

<p>Of course, I could take the easy path and pick a new platform like Hashnode or Ghost, but that would run the risk of finding myself back in the same place in a year or two when they inevitably change their terms and I have to migrate <em>again</em>.</p>

<p>Clearly, it was time to finally self-host.</p>

<p>The timing was right as well. A few years ago, going the self-hosting route would have required a significant time investment—from researching frameworks to spending hours on “just one little tweak” that would send me down a CSS rabbit hole. But these days, with the ubiquity of LLMs, I figured the process of creating a blog (very much a solved problem) should be trivial to accelerate.</p>

<p>Sure enough, my guess paid off. Within half an hour, I had all the information I needed to choose. My blog would be hosted on GitHub Pages using <a href="https://jekyllrb.com/">Jekyll</a> as the framework and <a href="https://mmistakes.github.io/minimal-mistakes/">Minimal Mistakes</a> for the theme.</p>

<h2 id="why-jekyll">Why Jekyll?</h2>

<p>When choosing a platform, I had a few criteria.</p>

<p>First and foremost, I wanted control over the platform and the content for the reasons I already mentioned. But within the world of self-hosting, there are a million ways to go, so I had to narrow things down.</p>

<p>As a busy (and somewhat lazy 🙃) developer, I didn’t want to spend three weeks building a custom React or Next.js blogging engine from scratch, nor did I want to navigate the mess of modern JavaScript framework fatigue just to display static text. I wanted something stable, battle-tested, inherently secure, and low-maintenance, requiring me to write as little code as possible.</p>

<p>Seeing how the theme of the blog is “Rabbi on Rails,” <strong>Jekyll</strong>, built in Ruby, was the obvious choice.</p>

<p>It integrates natively with GitHub Pages (meaning zero-cost hosting), uses standard Liquid templating, and handles simple Markdown files. To keep from reinventing the wheel layout-wise, I pulled in the <strong>Minimal Mistakes</strong> theme, which gave me a sleek, responsive design, an author sidebar, and clean typography right out of the box.</p>

<p>Most importantly, everything now lives in a <a href="https://github.com/achasveachas/rabbi-on-rails">Git repository on GitHub</a>. My blog posts are version-controlled. If I make a catastrophic formatting mistake, I don’t rely on a platform’s fragile history UI; I just run <code class="language-plaintext highlighter-rouge">git revert</code>. Plus, every post includes a built-in “Suggest a Fix” button that drops readers directly into the GitHub web editor to submit a PR. Even the reactions and comments on posts are handled via GitHub Discussions using <a href="https://giscus.app/">giscus</a>.</p>

<h2 id="scripting-the-migration-the-fun-part">Scripting The Migration: The Fun Part</h2>

<p>Setting up the blog using Jekyll was the easy part. Actually extracting years of data from two completely different platforms and standardizing it into Jekyll-compatible Markdown is where the real engineering happened.</p>

<p>Because I didn’t want to copy and paste dozens of articles manually, I wrote custom Ruby scripts to handle the heavy lifting (if you’re curious, you can find the scripts under the /scripts directory in the blog’s repo). I quickly discovered that Medium and DEV each have their own pain points that torment migrating developers in completely opposite ways:</p>

<h3 id="1-the-medium-export-the-html-swarm">1. The Medium Export (The HTML Swarm)</h3>
<p>When you export your data from Medium, they don’t give you clean Markdown. They hand you a zip file full of raw, clunky HTML files. My migration script had to parse through these HTML nodes, strip out Medium-specific wrappers, extract the metadata for the frontmatter, and translate the core elements back into clean Markdown. Furthermore, they don’t differentiate between different kinds of content: every blog post you wrote and every comment you ever left on anyone’s article all get dumped into one massive folder with no built-in way to tell them apart.</p>

<h3 id="2-the-devto-export-liquid-tag-fun">2. The DEV.to Export (Liquid Tag Fun)</h3>
<p>DEV.to is much friendlier on the surface; their export gives you actual Markdown files. However, the headache here was their proprietary ecosystem. DEV relies heavily on custom Liquid tags for embeds (like tweets, YouTube videos, GitHub gists, and other articles and comments on DEV). Because standard Jekyll doesn’t understand DEV’s specific tags, my script had to use a heavy dose of regex to scrub, translate, or strip those out into standard web embeds.</p>

<h3 id="3-the-asset-trap-owning-the-images">3. The Asset Trap (Owning the Images)</h3>
<p>One critical rule of a true migration: <strong>you must host your own images.</strong> If you leave your blog post images pointing to Medium or DEV’s CDNs, you don’t actually own your blog. If they change their CDN routing or delete your old account, your images break.</p>

<p>My script didn’t just transform text; it scanned every post, found the image URLs, downloaded the assets locally into an <code class="language-plaintext highlighter-rouge">assets/images/</code> directory, and rewrote the Markdown image paths to point to my repository.</p>

<h2 id="the-hyde-side-of-jekyll-the-honest-tradeoffs">The Hyde Side of Jekyll: The Honest Tradeoffs</h2>

<p>So far, the new setup is working fine for me, but every architectural change always comes with compromises. If you are thinking of making a similar move, you have to be comfortable with a few realities:</p>

<ul>
  <li><strong>No Native Platform Visibility:</strong> Medium and DEV have built-in audiences. When you publish on your own domain, you are shouting into the void until you distribute the link yourself. I plan on mitigating that a little by still cross-posting my technical posts on DEV (DEV makes it easy to add a canonical URL so your blog doesn’t take an SEO hit). Of course that means I need to do a better job promoting new blog posts on LinkedIn and other platforms.</li>
  <li><strong>No Slick Web Editor:</strong> Medium and DEV spoil you with smooth, browser-based WYSIWYG editors that feature instant previews and drag-and-drop image uploading. With a static blog, you say goodbye to all that. Writing a post now means opening your IDE, writing raw Markdown, manually saving image files into local asset folders, and pushing a git commit to publish. As a developer, I don’t mind writing in my local code editor anyway, and VSCode plugins provide useful features like quick previews and syntax support. But you definitely lose the convenience of logging into a website from any computer and firing off a quick post.</li>
  <li><strong>No Built-in Interactions:</strong> Out of the box, there are no “claps,” likes, or comment sections. In order to have a way for users to interact, I used giscus which relies on GitHub’s Discussions. That does mean that readers need a GitHub account to interact which works for an engineering crowd, but it means that my wife, who was my most consistent “clap” on Medium, probably won’t be reacting to my Jekyll posts anymore :)</li>
  <li><strong>The Analytics Void:</strong> Static sites on GitHub Pages have no backend server logs. If you want to track page views without involving massive, privacy-invasive trackers like Google Analytics, you have to get creative by switching to a server-side host like Cloudflare Pages or utilizing privacy-first, cookie-less scripts like GoatCounter. For me, that wasn’t worth the hassle; I write primarily for myself and if others find my posts useful that’s fine, so I’m okay just not collecting analytics and not knowing how many readers each blog post gets (if any).</li>
</ul>

<h2 id="the-wins-long-live-the-indie-web">The Wins: Long Live the Indie Web</h2>

<p>Despite the tradeoffs, the pros completely outweigh the cons.</p>

<p>I have total ownership. I own the layout, the content, the deployment pipeline, and the domain. Everything is centralized under <code class="language-plaintext highlighter-rouge">blog.yechiel.me</code>.</p>

<p>And the coolest unexpected side effect? The moment I published my first post, my RSS aggregator immediately pinged me. Because Jekyll natively builds a <code class="language-plaintext highlighter-rouge">feed.xml</code> file on every deploy, my RSS feed, which had been broken for ages by platform API shifts, instantly came back to life out of the box.</p>

<p>It feels incredibly good to step off the platform carousel and return to the roots of the indie web. If you’ve been putting off migrating your own content because of the technical friction, take this as your sign: write the script, own your data, and take your domain back.</p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="writing" /><category term="ruby" /><category term="jekyll" /><category term="blogging" /><summary type="html"><![CDATA[After years of splitting content between Medium and DEV, I finally moved everything to a self-hosted, version-controlled Jekyll site. Here is the why, the how, and the technical hurdles of the migration.]]></summary></entry><entry><title type="html">Hold Your Horses: A 200-Year-Old Lesson on AI</title><link href="https://blog.yechiel.me/hold-your-horses" rel="alternate" type="text/html" title="Hold Your Horses: A 200-Year-Old Lesson on AI" /><published>2026-06-30T20:00:00-04:00</published><updated>2026-06-30T20:00:00-04:00</updated><id>https://blog.yechiel.me/hold-your-horses</id><content type="html" xml:base="https://blog.yechiel.me/hold-your-horses"><![CDATA[<h3 id="to-ai-or-not-to-ai">To AI Or Not To AI</h3>

<p>If you follow the tech world these days, you’ve probably noticed that there is absolutely no middle ground when it comes to Artificial Intelligence.</p>

<p>On one side, you have the AI evangelists claiming that AGI is basically here, you’ll be replaced in six months (for the past two years), and you should be letting AI write your entire codebase based on vibes only or risk being left behind.</p>

<p>On the other side, you have the AI doomers who point at AI’s inability to count the “R”s in “strawberry” and its tendency to be wrong so confidently it puts mediocre white men to shame and proudly declare the technology to be a useless, glorified autocomplete.</p>

<p>The reality, as always, contains a little bit of both.</p>

<p><img src="https://media1.tenor.com/m/PLeWlGFYpIAAAAAd/both-is-good.gif" alt="both is good" /></p>

<p>AI is neither a magic wand that does your job for you and will solve all the world’s problems, nor is it completely useless. It could be a massive force multiplier. But in software engineering, as in life, speed is only useful if you are actually heading in the right direction.</p>

<h3 id="horsing-around">Horsing Around</h3>

<p>To understand where AI really fits into our toolbelts, I like to think of a story about Rabbi Shneur Zalman of Liadi known as the Ba’al HaTanya (“Author of the Tanya”) for his seminal work by that name.</p>

<p>Rabbi Shneur Zalman lived in present-day Belarus in the late 1700s and founded the Chabad chassidic movement. He was renowned not just as a mystic and a scholar, but as a deeply practical advisor. People would travel and correspond with him from all over to consult with him on their personal struggles.</p>

<p>One day, a father came to the Rebbe. His teenage son was turning away from his upbringing and values. Instead of focusing on his studies, the boy was spending all his time at the local stables, obsessing over horses and galloping around the countryside. The father wasn’t happy with his son’s new hobby and asked the Rebbe for advice.</p>

<p>The Rebbe advised the father to find an excuse to send the boy into town so they could meet. Knowing his son loved to ride, the father gave him an errand at the Rebbe’s court and told him he could ride his horse there. The young man happily galloped into town, eventually finding himself face-to-face with the Rebbe.</p>

<p>“Why did you choose to ride into town on a horse instead of taking a carriage?” the Rebbe asked.</p>

<p>“Because a good horse runs fast,” the boy replied enthusiastically. “You gallop away, and you reach your destination so much quicker.”</p>

<p>“That is all very well,” the Rebbe countered. “But if you make a wrong turn, then you end up traveling quickly in the wrong direction and get completely lost!”</p>

<p>The young man thought for a moment and pushed back. “That may be true, Rebbe,” he insisted, “but as soon as you catch yourself and see that you are on the wrong path, the horse can help you quickly get back on the road.”</p>

<p>“Yes,” the Rebbe repeated slowly and emphatically. “If you realize you’re on the wrong path before it is too late, you can quickly return to the main road…”</p>

<p><img src="/assets/images/posts/robotic-horse.jpg" alt="Scrap metal horse sculpture in a field." /></p>

<h3 id="the-artificial-horse">The Artificial Horse</h3>

<p>This story captures the reality of coding with Large Language Models.</p>

<p>LLMs give us unprecedented velocity, but they don’t actually understand <em>why</em> we are building things. It is raw, untamed horsepower.</p>

<p>Just the other day, I was using Claude to help write some controller tests for a web app. The test failed because the endpoint returned a <code class="language-plaintext highlighter-rouge">401 Unauthorized</code>, which made sense, we hadn’t set up an authenticated user in the test setup yet.</p>

<p>Instead of figuring out how to properly mock a logged-in user, Claude decided to take the fastest possible route to a green test suite. It rewrote the test to expect a <code class="language-plaintext highlighter-rouge">401 Unauthorized</code> and proudly declared the test passed “because we know the controller is working.”</p>

<p><img src="/assets/images/posts/task-failed.jpg" alt="A Windows dialogue that says &quot;Task failed successfully.&quot;" /></p>

<p>This is exactly what the Alter Rebbe meant. The AI horse galloped to the destination at lightning speed and got the test passing, but in completely the wrong direction.</p>

<p>If you just blindly click “Accept” (or worse, run Claude with <code class="language-plaintext highlighter-rouge">--dangerously-skip-permissions</code> 😱), AI will confidently help you write terrible code, introduce subtle (and not so subtle) security flaws, or “fix” tests by defeating their entire purpose, all faster than you can type.</p>

<h3 id="hold-the-reins-tight">Hold The Reins Tight</h3>

<p>But the young man in the story was right as well: if you realize you’ve made a mistake, the horse helps you pivot fast.</p>

<p>An experienced engineer will know when to pull the reins. When Claude tried “fixing” my test and I told it, <em>“No, we need to actually authenticate a user before making the request.”</em> It instantly generated the correct setup block, and got us back on the right path just as quickly as we had left it.</p>

<p>The horse isn’t the enemy, but it also isn’t going to get you to your destination on its own.</p>

<p>In the hands of someone who doesn’t know how to code, AI is just a fast ride in the wrong direction. But in the hands of an engineer who knows its strengths and its limitations it is an incredible tool.</p>

<p>Don’t be the developer who blindly trusts the horse to do the architectural thinking for you. And don’t be the developer who stubbornly refuses to ride, insisting on walking on foot while everyone else is galloping.</p>

<p>Don’t fear the horse. Just keep your hands on the reins, constantly check your map and compass, and be the rider.</p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="torah-tech" /><category term="ai" /><category term="reflections" /><summary type="html"><![CDATA[Is AI a magic wand or a useless toy? Here is what a 200-year-old Chassidic story taught me about steering the AI hype and learning to ride the horse.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://blog.yechiel.me/assets/images/posts/robotic-horse.jpg" /><media:content medium="image" url="https://blog.yechiel.me/assets/images/posts/robotic-horse.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Who’s Going To RubyConf 2026?</title><link href="https://blog.yechiel.me/whos-going-to-rubyconf-2026" rel="alternate" type="text/html" title="Who’s Going To RubyConf 2026?" /><published>2026-06-08T20:00:00-04:00</published><updated>2026-06-08T20:00:00-04:00</updated><id>https://blog.yechiel.me/whos-going-to-rubyconf-2026</id><content type="html" xml:base="https://blog.yechiel.me/whos-going-to-rubyconf-2026"><![CDATA[<p><img src="/assets/images/posts/https___dev-to-uploads.s3.amazonaws.com_uploads_articles_zken2vpnjvczelii07bv.webp" alt="RubyConf logo" /></p>

<p>RubyConf holds a special place in my heart. It was the very first tech conference I attended after receiving a scholarship fresh out of Flatiron School back in 2017 (you can read about my experience <a href="https://blog.yechiel.me/rubyconf-is-nice-dab165842db5">here</a>), and then in 2021, it was the stage for my <a href="https://youtu.be/iuVPObgoxF8?si=kQODALd49BQvH3wl">first conference talk</a> in Denver.</p>

<p>Now, in another first, I joined the Program Committee for <a href="https://rubyconf.org/">RubyConf 2026</a> to help put the program together, and what a program it is! We have an absolutely amazing lineup this year, and I’m so excited to see it come to life!</p>

<p>Who else is planning on attending? Let’s make plans to meet up and say hi!</p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="" /><category term="" /><category term="ruby" /><category term="conferences" /><category term="writing" /><summary type="html"><![CDATA[Will I be seeing you in Vegas this year?]]></summary></entry><entry><title type="html">Shipping Your Machine: Building a Container in 50 Lines of Code (Part 2)</title><link href="https://blog.yechiel.me/shipping-your-machine-building-a-container-in-50-lines-of-code-part-2-4cm4" rel="alternate" type="text/html" title="Shipping Your Machine: Building a Container in 50 Lines of Code (Part 2)" /><published>2026-05-26T11:03:07-04:00</published><updated>2026-05-26T11:03:07-04:00</updated><id>https://blog.yechiel.me/shipping-your-machine-building-a-container-in-50-lines-of-code-part-2-4cm4</id><content type="html" xml:base="https://blog.yechiel.me/shipping-your-machine-building-a-container-in-50-lines-of-code-part-2-4cm4"><![CDATA[<h2 id="welcome-back-to-the-jailhouse">Welcome Back to the Jailhouse</h2>

<p>In <a href="/shipping-your-machine-building-a-container-in-60-lines-of-code-part-1-14ma">Part 1 of this series</a>, we built the foundation of our container using Go. We successfully used the <code class="language-plaintext highlighter-rouge">CLONE_NEWUTS</code> namespace and process forking to isolate our container’s hostname from the host machine.</p>

<p>But we still have a massive security flaw. Right now, if we drop into our container’s bash shell, we can still see all of the host’s files. We could easily <code class="language-plaintext highlighter-rouge">cd</code> straight out of our “isolated” environment and mess with the host machine.</p>

<p>Let’s lock it down.</p>

<h3 id="chroot-to-jail"><code class="language-plaintext highlighter-rouge">chroot</code> to Jail</h3>

<p>Linux has a wonderful system call called <code class="language-plaintext highlighter-rouge">chroot</code> (short for “change root”). It lets us change the root directory (<code class="language-plaintext highlighter-rouge">/</code>) for a given process. As far as the process is concerned, the directory we point <code class="language-plaintext highlighter-rouge">chroot</code> to <em>is the entire universe</em>. Anything outside of it simply doesn’t exist.</p>

<p>Let’s update our <code class="language-plaintext highlighter-rouge">child()</code> function to set the root directory to our current working directory:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">child</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Running in new child process %v </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">])</span>
	
	<span class="n">must</span><span class="p">(</span><span class="n">syscall</span><span class="o">.</span><span class="n">Sethostname</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"container"</span><span class="p">)))</span>
	
	<span class="c">// Get current directory and lock the process inside it</span>
	<span class="n">pwd</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">os</span><span class="o">.</span><span class="n">Getwd</span><span class="p">()</span>
	<span class="n">must</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>

	<span class="n">must</span><span class="p">(</span><span class="n">syscall</span><span class="o">.</span><span class="n">Chroot</span><span class="p">(</span><span class="n">pwd</span><span class="p">))</span>
	<span class="c">// chroot changes the root, but doesn't automatically move us there. </span>
	<span class="c">// We must explicitly change our working directory to the new root!</span>
	<span class="n">must</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Chdir</span><span class="p">(</span><span class="s">"/"</span><span class="p">))</span>
	
	<span class="n">cmd</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="p">],</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">3</span><span class="o">:</span><span class="p">]</span><span class="o">...</span><span class="p">)</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdin</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdin</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdout</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdout</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stderr</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stderr</span>
	
	<span class="n">must</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">Run</span><span class="p">())</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Run <code class="language-plaintext highlighter-rouge">sudo go run main.go run /bin/bash</code>.</p>

<p><strong>Crash!</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>panic: fork/exec /bin/bash: no such file or directory
</code></pre></div></div>
<p>What happened?</p>

<p>We just told our process that our current directory is the entire universe. So, when we ask <code class="language-plaintext highlighter-rouge">exec.Command</code> to run <code class="language-plaintext highlighter-rouge">/bin/bash</code>, it isn’t looking at your computer’s actual hard drive anymore. It is looking inside your project folder for a directory called <code class="language-plaintext highlighter-rouge">bin</code> containing an executable called <code class="language-plaintext highlighter-rouge">bash</code>.</p>

<p>Because our current directory doesn’t have those, it fails! We need an actual root filesystem to provide the basic binaries our shell expects.</p>

<p><em>(Note: Production container runtimes like runC actually use a more advanced system call called <code class="language-plaintext highlighter-rouge">pivot_root</code> for better security, but <code class="language-plaintext highlighter-rouge">chroot</code> is perfect for understanding the core concept!).</em></p>

<h3 id="the-image">The Image</h3>

<p>To fix this, we need to provide an actual root filesystem that contains the basic folders and binaries (like /bin/bash) that our shell expects.</p>

<p>You can grab a basic Ubuntu root filesystem yourself using Docker. Open a new terminal tab and run these exact commands in your project directory:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use docker to start a Ubuntu container and then export its filesystem to a compressed file called ubuntu.tar</span>
docker <span class="nb">export</span> <span class="si">$(</span>docker create ubuntu<span class="si">)</span> <span class="o">&gt;</span> ubuntu.tar

<span class="c"># Create a directory called ubuntu-rootfs and unzip your tar file into it</span>
<span class="nb">mkdir </span>ubuntu-rootfs
<span class="nb">tar</span> <span class="nt">-xf</span> ubuntu.tar <span class="nt">-C</span> ubuntu-rootfs
</code></pre></div></div>
<p>This creates a folder called <code class="language-plaintext highlighter-rouge">ubuntu-rootfs</code> containing a complete, brand-new Ubuntu file system.</p>

<p>Assuming you have that folder in your project directory, let’s change our <code class="language-plaintext highlighter-rouge">chroot</code> call to point to it as follows:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="n">must</span><span class="p">(</span><span class="n">syscall</span><span class="o">.</span><span class="n">Chroot</span><span class="p">(</span><span class="n">filepath</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">pwd</span><span class="p">,</span> <span class="s">"ubuntu-rootfs"</span><span class="p">)))</span>
	<span class="n">must</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Chdir</span><span class="p">(</span><span class="s">"/"</span><span class="p">))</span>

</code></pre></div></div>

<p>Now, when we run <code class="language-plaintext highlighter-rouge">sudo go run main.go run /bin/bash</code>, everything works perfectly!</p>

<p>You can run <code class="language-plaintext highlighter-rouge">ls /</code> and you will only see the files inside your <code class="language-plaintext highlighter-rouge">ubuntu-rootfs</code> directory. Try running <code class="language-plaintext highlighter-rouge">cd ..</code> to escape, and you will find yourself in the exact same directory as before. You cannot access the host machine at all.</p>

<h3 id="pids-and-proc">PIDs and /proc</h3>

<p>We’re ready for the next step. If you remember, when we ran <code class="language-plaintext highlighter-rouge">docker run -it ubuntu /bin/bash</code> back in the beginning of Part 1, one of the ways we could tell we were in an isolated container was by running <code class="language-plaintext highlighter-rouge">ps aux</code> and observing only two processes running with very low PIDs.</p>

<p>Let’s try to replicate that. While inside our new container, try running <code class="language-plaintext highlighter-rouge">ps aux</code> to view the running processes.</p>

<p>It breaks with an error:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Error, <span class="k">do </span>this: mount <span class="nt">-t</span> proc proc /proc
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">ps</code> command works by reading the <code class="language-plaintext highlighter-rouge">/proc</code> directory, which is a special virtual filesystem in Linux that contains live data about running processes. Our isolated root filesystem has an empty <code class="language-plaintext highlighter-rouge">/proc</code> folder, and the operating system hasn’t been told to attach the live process data to it. Because it’s empty, <code class="language-plaintext highlighter-rouge">ps</code> fails!</p>

<p>To fix this, we need to do two things:</p>

<ol>
  <li>Give our container its own isolated Process IDs (PIDs) using namespaces.</li>
  <li>Mount the <code class="language-plaintext highlighter-rouge">proc</code> filesystem so commands like <code class="language-plaintext highlighter-rouge">ps</code> can read it.</li>
</ol>

<p>First, update the <code class="language-plaintext highlighter-rouge">SysProcAttr</code> in the <code class="language-plaintext highlighter-rouge">run()</code> function to include the PID and Mount namespaces. (Note: <code class="language-plaintext highlighter-rouge">CLONE_NEWNS</code> stands for “New Namespace”, but it specifically refers to the Mount namespace! It just happens to be the first namespace added to the Linux kernel and back then no one thought they might end up needing more so they just called it “namespace” 🤷).</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="n">cmd</span><span class="o">.</span><span class="n">SysProcAttr</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">syscall</span><span class="o">.</span><span class="n">SysProcAttr</span><span class="p">{</span>
		<span class="n">Cloneflags</span><span class="o">:</span> <span class="n">syscall</span><span class="o">.</span><span class="n">CLONE_NEWUTS</span> <span class="o">|</span> <span class="n">syscall</span><span class="o">.</span><span class="n">CLONE_NEWPID</span> <span class="o">|</span> <span class="n">syscall</span><span class="o">.</span><span class="n">CLONE_NEWNS</span><span class="p">,</span>
	<span class="p">}</span>

</code></pre></div></div>

<p>Next, mount the <code class="language-plaintext highlighter-rouge">proc</code> directory inside our <code class="language-plaintext highlighter-rouge">child()</code> function, right after we <code class="language-plaintext highlighter-rouge">chroot</code>. We will also use Go’s <code class="language-plaintext highlighter-rouge">defer</code> keyword to ensure we unmount it and clean up after ourselves when the function exits:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="n">must</span><span class="p">(</span><span class="n">syscall</span><span class="o">.</span><span class="n">Chroot</span><span class="p">(</span><span class="n">filepath</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">pwd</span><span class="p">,</span> <span class="s">"ubuntu-rootfs"</span><span class="p">)))</span>
	<span class="n">must</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Chdir</span><span class="p">(</span><span class="s">"/"</span><span class="p">))</span>
	
	<span class="c">// Mount the proc filesystem</span>
	<span class="n">must</span><span class="p">(</span><span class="n">syscall</span><span class="o">.</span><span class="n">Mount</span><span class="p">(</span><span class="s">"proc"</span><span class="p">,</span> <span class="s">"proc"</span><span class="p">,</span> <span class="s">"proc"</span><span class="p">,</span> <span class="m">0</span><span class="p">,</span> <span class="s">""</span><span class="p">))</span>
	<span class="c">// Clean up after ourselves when the function exits</span>
	<span class="k">defer</span> <span class="n">syscall</span><span class="o">.</span><span class="n">Unmount</span><span class="p">(</span><span class="s">"proc"</span><span class="p">,</span> <span class="m">0</span><span class="p">)</span>
</code></pre></div></div>

<p>Now, run your container and type <code class="language-plaintext highlighter-rouge">ps aux</code>. You’ll see only three processes running: <code class="language-plaintext highlighter-rouge">exe</code> (our Go program) running as PID 1, <code class="language-plaintext highlighter-rouge">bash</code> running as PID 2, and the <code class="language-plaintext highlighter-rouge">ps</code> command we just ran!</p>

<h3 id="cgroups-keeping-it-civil">Cgroups (Keeping it Civil)</h3>

<p>We have our invisibility cloak (Namespaces) and our isolated universe (<code class="language-plaintext highlighter-rouge">chroot</code>). But what happens if we write an infinite <code class="language-plaintext highlighter-rouge">while</code> loop inside our container that eats up all the CPU and memory?</p>

<p>It would completely crash the host machine!</p>

<p>To prevent our container from using up all of our resources, Linux uses <strong>cgroups</strong> (Control Groups). Cgroups act as the bouncer, ensuring no single container uses more than its fair share of resources.</p>

<p>To set up a cgroup, we can lean on a famous Linux philosophy: “Everything is a file.” This means we can configure the kernel’s resource limits by creating specific directories and writing text into special files.</p>

<p>Let’s add a quick helper function to our <code class="language-plaintext highlighter-rouge">main.go</code> file to limit the maximum number of processes our container is allowed to spawn to 20:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">cg</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">cgroups</span> <span class="o">:=</span> <span class="s">"/sys/fs/cgroup/"</span>
	<span class="n">pids</span> <span class="o">:=</span> <span class="n">filepath</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">cgroups</span><span class="p">,</span> <span class="s">"pids"</span><span class="p">)</span>
	
	<span class="c">// 1. Create a new cgroup for our container</span>
	<span class="n">containerCgroup</span> <span class="o">:=</span> <span class="n">filepath</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">pids</span><span class="p">,</span> <span class="s">"my-container"</span><span class="p">)</span>
	<span class="n">os</span><span class="o">.</span><span class="n">Mkdir</span><span class="p">(</span><span class="n">containerCgroup</span><span class="p">,</span> <span class="m">0755</span><span class="p">)</span>
	
	<span class="c">// 2. Write the limit into the cgroup file (max 20 processes)</span>
	<span class="n">must</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">WriteFile</span><span class="p">(</span><span class="n">filepath</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">containerCgroup</span><span class="p">,</span> <span class="s">"pids.max"</span><span class="p">),</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"20"</span><span class="p">),</span> <span class="m">0700</span><span class="p">))</span>
	
	<span class="c">// 3. Add our current process to this cgroup</span>
	<span class="n">must</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">WriteFile</span><span class="p">(</span><span class="n">filepath</span><span class="o">.</span><span class="n">Join</span><span class="p">(</span><span class="n">containerCgroup</span><span class="p">,</span> <span class="s">"cgroup.procs"</span><span class="p">),</span> <span class="p">[]</span><span class="kt">byte</span><span class="p">(</span><span class="n">strconv</span><span class="o">.</span><span class="n">Itoa</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Getpid</span><span class="p">())),</span> <span class="m">0700</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s break down what this function is doing:</p>

<ol>
  <li>
    <p><strong>Create the group:</strong> By making a new directory inside <code class="language-plaintext highlighter-rouge">/sys/fs/cgroup/pids</code>, the Linux kernel automatically creates a new Control Group for us.</p>
  </li>
  <li>
    <p><strong>Set the rule:</strong> Inside that new directory, Linux automatically generates a file called <code class="language-plaintext highlighter-rouge">pids.max</code>. We open that file and write the text <code class="language-plaintext highlighter-rouge">"20"</code> into it. This establishes a rule that our process will only be allowed to run 20 sub-processes.</p>
  </li>
  <li>
    <p><strong>Enforce the rule:</strong> Linux also generates a file called <code class="language-plaintext highlighter-rouge">cgroup.procs</code>. We get our Go program’s current Process ID (<code class="language-plaintext highlighter-rouge">os.Getpid()</code>) and write it into this file. This tells the kernel, <em>“Hey, apply the rules of this folder to me!”</em></p>
  </li>
</ol>

<p>Finally, let’s call this function inside our <code class="language-plaintext highlighter-rouge">run()</code> function, right before we execute our child process:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">run</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Running %v </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">])</span>
	
	<span class="n">args</span> <span class="o">:=</span> <span class="nb">append</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">"child"</span><span class="p">},</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">]</span><span class="o">...</span><span class="p">)</span>
	<span class="n">cmd</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="s">"/proc/self/exe"</span><span class="p">,</span> <span class="n">args</span><span class="o">...</span><span class="p">)</span>
	
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdin</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdin</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdout</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdout</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stderr</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stderr</span>
	
	<span class="n">cmd</span><span class="o">.</span><span class="n">SysProcAttr</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">syscall</span><span class="o">.</span><span class="n">SysProcAttr</span><span class="p">{</span>
		<span class="n">Cloneflags</span><span class="o">:</span> <span class="n">syscall</span><span class="o">.</span><span class="n">CLONE_NEWUTS</span> <span class="o">|</span> <span class="n">syscall</span><span class="o">.</span><span class="n">CLONE_NEWPID</span> <span class="o">|</span> <span class="n">syscall</span><span class="o">.</span><span class="n">CLONE_NEWNS</span><span class="p">,</span>
	<span class="p">}</span>
	
	<span class="c">// Set up our resource limits!</span>
	<span class="n">cg</span><span class="p">()</span>
	
	<span class="n">must</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">Run</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And just like that, our container is officially resource-limited! Because cgroups inherit down to child processes, everything that runs inside our container is bound by this rule. If a malicious script inside tries to execute a “fork-bomb” (a script that endlessly copies itself to freeze the computer), the kernel will step in and aggressively kill it the second it hits 20 processes.</p>

<h3 id="the-reveal">The Reveal</h3>

<p>If you put all of this together, we just built Docker from scratch in about 50 lines of code.</p>

<p>Containers aren’t magic. They aren’t heavyweight VMs. They are simply standard Linux processes wrapped in namespaces, jailed in a specific directory, and policed by cgroups.</p>

<p>In fact, you don’t even need Go to do this. You can trigger the exact same isolation using a single line of bash:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>unshare <span class="nt">--uts</span> <span class="nt">--pid</span> <span class="nt">--mount</span> <span class="nt">--fork</span> <span class="nt">--root</span><span class="o">=</span>/home/ubuntu-rootfs <span class="nt">--mount-proc</span> /bin/bash

</code></pre></div></div>

<p>And there you have it! The next time someone says “it works on my machine,” you know exactly what it takes to ship their machine to production.</p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="containers" /><category term="go" /><category term="command-line" /><category term="tutorial" /><summary type="html"><![CDATA[Learn about the fascinating world of Linux containers by building one from scratch.]]></summary></entry><entry><title type="html">Shipping Your Machine: Building a Container in 50 Lines of Code (Part 1)</title><link href="https://blog.yechiel.me/shipping-your-machine-building-a-container-in-60-lines-of-code-part-1-14ma" rel="alternate" type="text/html" title="Shipping Your Machine: Building a Container in 50 Lines of Code (Part 1)" /><published>2026-05-20T16:09:10-04:00</published><updated>2026-05-20T16:09:10-04:00</updated><id>https://blog.yechiel.me/shipping-your-machine-building-a-container-in-60-lines-of-code-part-1-14ma</id><content type="html" xml:base="https://blog.yechiel.me/shipping-your-machine-building-a-container-in-60-lines-of-code-part-1-14ma"><![CDATA[<h2 id="the-works-on-my-machine-problem">The “Works on My Machine” Problem</h2>

<p>We’ve all been there. You spend days writing a new feature. You test it locally, everything passes, you push it to production, and… <em>boom</em>. It crashes immediately.</p>

<p><em>“But it works on my machine!”</em> you cry out to your lead engineer.</p>

<p><img src="/assets/images/posts/2026-05-20-shipping-your-machine-building-a-container-in-60-lines-of-code-part-1-14ma-36eaa8.webp" alt="A crying child tells an adult sitting next to him &quot;but it works on my machine&quot;, the adult tells him &quot;then we'll ship your machine&quot;, and that's how Docker was born." /></p>

<p>And that is exactly what containers are. They are a way to bundle up your application and the environment it runs in—your machine—so that it behaves exactly the same way in production as it does on your laptop.</p>

<p>But what <em>is</em> a container, really?</p>

<p>Most people have an intuition of containers as some kind of complicated application that runs on your computer and simulates another computer. That is the image I had in my head even after working on containers for a few months at Pivotal/VMware… <strong>and it’s a myth.</strong></p>

<p>The reality is a lot simpler, and a lot more interesting!</p>

<p>The truth is that a container is simply a directory on your computer—like any other directory—with a process running inside of it. What makes it a container and not just another process running inside a directory is that we use some clever built-in Linux features to trick the process into thinking that this directory <strong>is the entire computer</strong>. As far as this process is concerned, nothing exists outside of the directory we “trapped” it in and that directory is the entirety of its universe.</p>

<p>In this two-part series, we are going to demystify this illusion by building Docker from scratch in exactly 60 lines of Go code.</p>

<p><strong>Note:</strong> <em>Because containers rely heavily on the Linux Kernel, this tutorial will only run natively on a Linux machine. If you are following along on a Mac, you’ll need to spin up a Linux VM first, as Macs run on the Darwin kernel and don’t have these system calls!</em></p>

<p>Let’s get started!</p>

<h3 id="setting-the-stage">Setting the Stage</h3>

<p>We’ll be writing our container in Go. Why Go? Because it gives us incredibly clean access to underlying Linux system calls, which we’ll need to create our container illusion. This is the reason Docker, Kubernetes, and other cloud-native projects are all written in Go.</p>

<p>Let’s try to replicate the core behavior of Docker.</p>

<p>Normally, when using Docker, you run a command like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> ubuntu /bin/bash
</code></pre></div></div>

<p>If you run that (assuming you have docker installed) you will see that you have been dropped into a new shell. This shell looks different than your original shell and you can tell it’s completely isolated from the rest of your machine (the host) in a few ways (it’s worth opening a new tab in your terminal so you can see these changes side by side):</p>

<ol>
  <li>
    <p><strong>Your prompt:</strong> Everyone’s prompt is different, but most have something in the beginning that looks like <code class="language-plaintext highlighter-rouge">[username]@[hostname]</code>. The prompt you see now probably looks like <code class="language-plaintext highlighter-rouge">root@[some-random-string]</code>.</p>
  </li>
  <li>
    <p><strong>Your hostname:</strong> If you type <code class="language-plaintext highlighter-rouge">hostname</code> in your host computer’s terminal, it will output your computer’s actual name. If you run <code class="language-plaintext highlighter-rouge">hostname</code> inside your container, on the other hand, you will see that same random sequence of characters from your prompt. Docker assigned this fake hostname to your container at random.</p>
  </li>
  <li>
    <p><strong>Your File System:</strong> If you type <code class="language-plaintext highlighter-rouge">ls /</code> inside your container you will see that none of the files from your computer’s actual root directory are there, instead you will see a fresh list of files and directories, exactly like you would find in a brand new Ubuntu installation.</p>
  </li>
  <li>
    <p><strong>Your processes:</strong> If you type <code class="language-plaintext highlighter-rouge">ps aux</code> in your container you will find only 2 processes with very low Process IDs (PIDs)—typically PID 1 for the shell process you’re in and another low number PID for the <code class="language-plaintext highlighter-rouge">ps</code> command you just ran. Meanwhile, running <code class="language-plaintext highlighter-rouge">ps aux</code> on your host machine will show a massive list of running processes, many of them with very high PIDs.</p>
  </li>
</ol>

<p>We will try to replicate this behavior from scratch, with a few minor differences.</p>

<p>Our goal is to run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>go run main.go run /bin/bash
</code></pre></div></div>

<p>And if we do everything right the behavior will be mostly the same. We will drop into a new shell where:</p>

<ol>
  <li>
    <p>We will see a different hostname than our existing one.</p>
  </li>
  <li>
    <p>The root directory we will have access to will not be the root directory of our computer, instead it will be a new root directory.</p>
  </li>
  <li>
    <p>If we inspect the processes running using <code class="language-plaintext highlighter-rouge">ps</code> we will see only the processes running in our program and not all the processes on our computer.</p>
  </li>
</ol>

<p>So let’s start!</p>

<h3 id="show-me-the-code">Show Me the Code</h3>

<p>Let’s build this step by step. Create a new directory on your Linux computer, make a file named main.go, and let’s add our initial boilerplate:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"fmt"</span>
	<span class="s">"os"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="c">// os.Args is a list of everything typed in the terminal.</span>
	<span class="c">// [0] is the program name (main.go), [1] is our command ("run")</span>
	<span class="k">switch</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">1</span><span class="p">]</span> <span class="p">{</span>
	<span class="k">case</span> <span class="s">"run"</span><span class="o">:</span>
		<span class="n">run</span><span class="p">()</span>
	<span class="k">default</span><span class="o">:</span>
		<span class="nb">panic</span><span class="p">(</span><span class="s">"Invalid argument"</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">run</span><span class="p">()</span> <span class="p">{</span>
	<span class="c">// os.Args[2:] takes everything AFTER the "run" command</span>
	<span class="c">// e.g., ["echo", "hello", "world"]</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Running %v </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">])</span>
<span class="p">}</span>

</code></pre></div></div>

<p>In Go, <code class="language-plaintext highlighter-rouge">os.Args</code> captures every word you type into the terminal as a list (a slice) of strings. We are telling our program to look at the second word (<code class="language-plaintext highlighter-rouge">os.Args[1]</code>). If it says “run”, we trigger our <code class="language-plaintext highlighter-rouge">run()</code> function which, for now does nothing but print all our arguments to the terminal.</p>

<p>If we run this in our terminal:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>go run main.go run <span class="nb">echo </span>hello world
Running <span class="o">[</span><span class="nb">echo </span>hello world]

</code></pre></div></div>

<p>Awesome. It successfully reads our command. But it’s just printing text; it’s not actually executing the command yet.</p>

<p>To make it execute the command, we need to wire up Go’s <code class="language-plaintext highlighter-rouge">os/exec</code> package (a package for executing commands on our computer). Let’s update our file:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"fmt"</span>
	<span class="s">"os"</span>
	<span class="s">"os/exec"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">switch</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">1</span><span class="p">]</span> <span class="p">{</span>
	<span class="k">case</span> <span class="s">"run"</span><span class="o">:</span>
		<span class="n">run</span><span class="p">()</span>
	<span class="k">default</span><span class="o">:</span>
		<span class="nb">panic</span><span class="p">(</span><span class="s">"Invalid argument"</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">run</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Running %v </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">])</span>
	
        <span class="c">// 1. Define the command we want to execute. This includes the third element in our array </span>
	<span class="c">// (the actual command, in our case "echo"), as well as any optional arguments we may </span>
	<span class="c">// want to pass to it ("hello world").</span>
	<span class="n">cmd</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="p">],</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">3</span><span class="o">:</span><span class="p">]</span><span class="o">...</span><span class="p">)</span>
	
	<span class="c">// 2. Wire up the plumbing</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdin</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdin</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdout</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdout</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stderr</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stderr</span>
	
	<span class="c">// 3. Run the command and handle any errors</span>
	<span class="n">must</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">Run</span><span class="p">())</span>
<span class="p">}</span>

<span class="c">// A tiny helper function to catch errors and crash the program </span>
<span class="c">// cleanly if something goes wrong.</span>
<span class="k">func</span> <span class="n">must</span><span class="p">(</span><span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Let’s look at what we just added to <code class="language-plaintext highlighter-rouge">run()</code>:</p>

<ol>
  <li>
    <p><strong>Define the command:</strong> <code class="language-plaintext highlighter-rouge">exec.Command</code> takes the program we want to run (like <code class="language-plaintext highlighter-rouge">echo</code>) and any arguments we want to pass to it (<code class="language-plaintext highlighter-rouge">hello world</code>).</p>
  </li>
  <li>
    <p><strong>Wire up the plumbing:</strong> This part is crucial. By default, when a program spins up a new process, it runs invisibly in the background. By pointing the new command’s Standard Input, Output, and Error to our own <code class="language-plaintext highlighter-rouge">os.Stdin</code>, <code class="language-plaintext highlighter-rouge">os.Stdout</code>, and <code class="language-plaintext highlighter-rouge">os.Stderr</code>, we are attaching its “mouth and ears” directly to our terminal so we can actually interact with it.</p>
  </li>
  <li>
    <p><strong>Run it:</strong> We execute the command. Because Go requires explicit error handling, we wrapped it in a tiny <code class="language-plaintext highlighter-rouge">must()</code> helper function to keep our code readable.</p>
  </li>
</ol>

<p>Let’s test it out:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>go run main.go run <span class="nb">echo </span>hello world
Running <span class="o">[</span><span class="nb">echo </span>hello world]
hello world
</code></pre></div></div>

<p>We actually get <code class="language-plaintext highlighter-rouge">hello world</code> echoed back at us by the system!</p>

<p>Even better, we can tell our program to drop us into an interactive shell:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>go run main.go run /bin/bash
Running <span class="o">[</span>/bin/bash]
root@your-computer:/home/yechiel/docker-clone# 
</code></pre></div></div>

<p>However, this shell is absolutely not a container yet. If you type <code class="language-plaintext highlighter-rouge">hostname</code>, you’ll see your host machine’s actual name. If you type <code class="language-plaintext highlighter-rouge">ls /</code>, you can see all your host files. Right now, we have just written a fancy Go wrapper around a regular bash process.</p>

<p>It is time to start building the walls of our container.</p>

<h3 id="introducing-namespaces-the-invisibility-cloak">Introducing Namespaces (The Invisibility Cloak)</h3>

<p>To isolate our process from the rest of the computer, we need to put it inside a <strong>Namespace</strong>. A namespace is like an invisibility cloak that hides the rest of the computer from our process.</p>

<p>There are different namespaces that isolate different aspects of the system. Let’s start with the UTS namespace, which isolates the hostname.</p>

<p>To create a new namespace, we pass a specific “clone flag” to the system call spinning up our process. Update your <code class="language-plaintext highlighter-rouge">run()</code> function:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">run</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Running %v </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">])</span>
	
	<span class="n">cmd</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="p">],</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">3</span><span class="o">:</span><span class="p">]</span><span class="o">...</span><span class="p">)</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdin</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdin</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdout</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdout</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stderr</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stderr</span>
	
	<span class="c">// Set up the namespace!</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">SysProcAttr</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">syscall</span><span class="o">.</span><span class="n">SysProcAttr</span><span class="p">{</span>
		<span class="n">Cloneflags</span><span class="o">:</span> <span class="n">syscall</span><span class="o">.</span><span class="n">CLONE_NEWUTS</span><span class="p">,</span>
	<span class="p">}</span>
	
	<span class="n">must</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">Run</span><span class="p">())</span>
<span class="p">}</span>

</code></pre></div></div>

<p>If you try to run the command now, you’ll get a “permission denied” error. Because changing a namespace requires modifying kernel-level structures, you now need root privileges.</p>

<p>Instead, let’s run <code class="language-plaintext highlighter-rouge">sudo go run main.go run /bin/bash</code>.</p>

<p>If you run <code class="language-plaintext highlighter-rouge">hostname</code> now you will still see your computer’s actual hostname. This is because, by default, Linux has your new process inherit the hostname of the host computer.</p>

<p>However, if you manually run <code class="language-plaintext highlighter-rouge">hostname container</code> inside this new bash shell, and <em>then</em> type <code class="language-plaintext highlighter-rouge">hostname</code>, you’ll see your hostname is now <code class="language-plaintext highlighter-rouge">container</code>. But if you switch to a terminal tab on your host machine and type <code class="language-plaintext highlighter-rouge">hostname</code>, you’ll see your actual computer’s name hasn’t changed at all! Our process is successfully isolated.</p>

<h3 id="forking-the-process">Forking the Process</h3>

<p>Did you notice something weird when you manually changed the hostname in the last step? Even though typing <code class="language-plaintext highlighter-rouge">hostname</code> printed out <code class="language-plaintext highlighter-rouge">container</code>, your actual bash prompt likely still said <code class="language-plaintext highlighter-rouge">root@your-computer#</code>!</p>

<p>This happens because the bash shell reads the machine’s hostname <em>when it first starts up.</em> Changing the hostname after the shell is already running doesn’t dynamically update your prompt. If we want our container to feel like a truly isolated environment from the second it boots, we need our Go program to change the hostname automatically <em>before</em> we execute /bin/bash.</p>

<p>Luckily, Go has a function to change the hostname: <code class="language-plaintext highlighter-rouge">syscall.Sethostname()</code>.</p>

<p>But here we run into a “chicken and egg” problem: where do we call that function?</p>

<p>If we call it before <code class="language-plaintext highlighter-rouge">cmd.Run()</code>, it executes on our host machine and changes our actual computer’s name (bad!). If we call it after <code class="language-plaintext highlighter-rouge">cmd.Run()</code>, it’ll only run after our bash shell has already exited (still bad!).</p>

<p>We need a middleman. We need our Go program to cross over into the new namespace, set the hostname, and <em>then</em> execute the <code class="language-plaintext highlighter-rouge">/bin/bash</code> command.</p>

<p>To do this, we are going to have our Go program run <strong>itself</strong> again as a child process.</p>

<p>Let’s update our <code class="language-plaintext highlighter-rouge">main.go</code> file. We will update the <code class="language-plaintext highlighter-rouge">main()</code> and <code class="language-plaintext highlighter-rouge">run()</code> functions, and add a brand new <code class="language-plaintext highlighter-rouge">child()</code> function:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"fmt"</span>
	<span class="s">"os"</span>
	<span class="s">"os/exec"</span>
	<span class="s">"syscall"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">switch</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">1</span><span class="p">]</span> <span class="p">{</span>
	<span class="k">case</span> <span class="s">"run"</span><span class="o">:</span>
		<span class="n">run</span><span class="p">()</span>
	<span class="k">case</span> <span class="s">"child"</span><span class="o">:</span>
		<span class="n">child</span><span class="p">()</span> <span class="c">// We added a new command here!</span>
	<span class="k">default</span><span class="o">:</span>
		<span class="nb">panic</span><span class="p">(</span><span class="s">"Invalid argument"</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">run</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Running %v </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">])</span>
	
	<span class="c">// /proc/self/exe is a special Linux file that points to the program </span>
	<span class="c">// currently running. So, our program is calling itself!</span>
	<span class="c">// We pass "child" as the first argument, followed by the rest of our commands.</span>
	<span class="n">args</span> <span class="o">:=</span> <span class="nb">append</span><span class="p">([]</span><span class="kt">string</span><span class="p">{</span><span class="s">"child"</span><span class="p">},</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">]</span><span class="o">...</span><span class="p">)</span>
	<span class="n">cmd</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="s">"/proc/self/exe"</span><span class="p">,</span> <span class="n">args</span><span class="o">...</span><span class="p">)</span>
	
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdin</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdin</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdout</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdout</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stderr</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stderr</span>
	
	<span class="n">cmd</span><span class="o">.</span><span class="n">SysProcAttr</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">syscall</span><span class="o">.</span><span class="n">SysProcAttr</span><span class="p">{</span>
		<span class="n">Cloneflags</span><span class="o">:</span> <span class="n">syscall</span><span class="o">.</span><span class="n">CLONE_NEWUTS</span><span class="p">,</span>
	<span class="p">}</span>
	
	<span class="n">must</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">Run</span><span class="p">())</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">child</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Running in new child process %v </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="o">:</span><span class="p">])</span>
	
	<span class="c">// We are now inside the namespace! It is safe to change the hostname.</span>
	<span class="n">must</span><span class="p">(</span><span class="n">syscall</span><span class="o">.</span><span class="n">Sethostname</span><span class="p">([]</span><span class="kt">byte</span><span class="p">(</span><span class="s">"container"</span><span class="p">)))</span>
	
	<span class="c">// Now we run the actual command the user requested (like /bin/bash)</span>
	<span class="n">cmd</span> <span class="o">:=</span> <span class="n">exec</span><span class="o">.</span><span class="n">Command</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">2</span><span class="p">],</span> <span class="n">os</span><span class="o">.</span><span class="n">Args</span><span class="p">[</span><span class="m">3</span><span class="o">:</span><span class="p">]</span><span class="o">...</span><span class="p">)</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdin</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdin</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stdout</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stdout</span>
	<span class="n">cmd</span><span class="o">.</span><span class="n">Stderr</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">Stderr</span>
	
	<span class="n">must</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">Run</span><span class="p">())</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">must</span><span class="p">(</span><span class="n">err</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="nb">panic</span><span class="p">(</span><span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Let’s break down the magic happening in <code class="language-plaintext highlighter-rouge">exec.Command("/proc/self/exe", args...)</code>:</p>

<ol>
  <li>
    <p><code class="language-plaintext highlighter-rouge">"/proc/self/exe"</code>: In Linux, <code class="language-plaintext highlighter-rouge">/proc</code> is a special directory that holds information about all running processes. If you check what’s in there (by running <code class="language-plaintext highlighter-rouge">ls /proc</code>) you will see a whole bunch of directories that have numbers as names. Each number is a PID and that directory contains information about the process with that PID. <code class="language-plaintext highlighter-rouge">/proc/self/exe</code> is essentially a shortcut to the directory for the process that’s currently running (in our case <code class="language-plaintext highlighter-rouge">go run main.go</code>).</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">args...</code>: We are taking the word <code class="language-plaintext highlighter-rouge">"child"</code> and appending the rest of the user’s commands (like <code class="language-plaintext highlighter-rouge">/bin/bash</code>) to it. The <code class="language-plaintext highlighter-rouge">...</code> syntax in Go simply “unpacks” the list so it can be passed as individual arguments.</p>
  </li>
</ol>

<p>So, when you type <code class="language-plaintext highlighter-rouge">go run main.go run /bin/bash</code>, here is what happens:</p>

<ol>
  <li>
    <p>The program starts, sees <code class="language-plaintext highlighter-rouge">"run"</code>, and triggers the <code class="language-plaintext highlighter-rouge">run()</code> function.</p>
  </li>
  <li>
    <p>The <code class="language-plaintext highlighter-rouge">run()</code> function sets up the invisibility cloak (the <code class="language-plaintext highlighter-rouge">NEWUTS</code> namespace).</p>
  </li>
  <li>
    <p>Inside that cloak, it runs itself again, but this time passing the command <code class="language-plaintext highlighter-rouge">"child"</code>.</p>
  </li>
  <li>
    <p>The program starts a second time, sees <code class="language-plaintext highlighter-rouge">"child"</code>, and triggers the <code class="language-plaintext highlighter-rouge">child()</code> function.</p>
  </li>
  <li>
    <p>Because we are now safely inside the namespace, it changes the hostname to <code class="language-plaintext highlighter-rouge">container</code> and finally runs <code class="language-plaintext highlighter-rouge">/bin/bash</code>.</p>
  </li>
</ol>

<p>Let’s test it. Run <code class="language-plaintext highlighter-rouge">sudo go run main.go run /bin/bash</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">sudo </span>go run main.go run /bin/bash
Running <span class="o">[</span>/bin/bash]
Running <span class="k">in </span>new child process <span class="o">[</span>/bin/bash]
root@container:/home/yechiel/docker-clone# 
</code></pre></div></div>

<p>Look at that prompt! We’ve successfully isolated the hostname programmatically before bash loaded. You’ll see both print statements execute, and if you type <code class="language-plaintext highlighter-rouge">hostname</code> in the new shell, it will say <code class="language-plaintext highlighter-rouge">container</code>.</p>

<p>We are still not in a proper container, though. If you run <code class="language-plaintext highlighter-rouge">ls /</code>, you will see all the files on your host machine, and you can even use <code class="language-plaintext highlighter-rouge">cd ..</code> to “escape” the directory entirely. Worse, if you were running this in a shared cloud environment, you would theoretically be able to see everyone else’s files and running processes—a massive security risk! We need a way to lock our process down so it can’t escape its designated environment.</p>

<p>In Part 2, we will tackle the final pieces of the container puzzle: jailing the file system, isolating the Process IDs (PIDs) so our container can only see itself, and stopping infinite loops from crashing the host computer using <code class="language-plaintext highlighter-rouge">cgroups</code>. Stay tuned!</p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="containers" /><category term="go" /><category term="command-line" /><category term="tutorial" /><summary type="html"><![CDATA[Learn about the fascinating world of Linux containers by building one from scratch.]]></summary></entry><entry><title type="html">Refactoring Your Bitachon: Moving From Monolith to Modular</title><link href="https://blog.yechiel.me/decoupling-your-livelihood-79c7a122a44d" rel="alternate" type="text/html" title="Refactoring Your Bitachon: Moving From Monolith to Modular" /><published>2025-12-22T12:04:12-05:00</published><updated>2025-12-22T12:04:12-05:00</updated><id>https://blog.yechiel.me/2025-12-22_Refactoring-Your-Bitachon--Moving-From-Monolith-to-Modular</id><content type="html" xml:base="https://blog.yechiel.me/decoupling-your-livelihood-79c7a122a44d"><![CDATA[<hr />

<h3 id="refactoring-your-bitachon-moving-from-monolith-tomodular">Refactoring Your Bitachon: Moving From Monolith to Modular</h3>

<p>I was recently learning <strong>Shaar HaBitachon</strong> (“<em>The Gate of Trust</em>”) — a section of the classic 11th-century work Chovot Halevavot (“<em>Duties of the Heart”)</em> by Rabbeinu Bachya ibn Pekuda dedicated to cultivating trust in G-d — with my Chavruta (study partner). (By the way, shout-out to this amazing book! If you aren’t learning it yet, I highly recommend picking it up! It’s a game-changer for your mindset!)</p>

<p>We were learning a part in <a href="https://www.chabad.org/library/article_cdo/aid/5478430/jewish/Chapter-Five-Part-4-Proper-Attitude.htm"><strong>Chapter Five</strong></a> which discusses the proper attitude one should have towards the “means” (<em>hishtadlut</em>, or personal effort) we employ to make a living.</p>

<p>Rabbeinu Bachya, the author, explains that there is a fundamental difference between someone who has Bitachon (trust in G-d) and someone who trusts in their own efforts:</p>

<blockquote>
  <p><em>“While a person who relies on G-d also involves himself in various means of obtaining his livelihood… he doesn’t rely on them, nor does he expect them to either benefit him or cause him harm unless G-d wills it to be.</em></p>
</blockquote>

<blockquote>
  <p><em>The only reason he involves himself in them is his choice to carry out the service of the Creator, Who instructed him to involve himself in the world…”</em></p>
</blockquote>

<p>Contrast this with the person <strong>without Bitachon</strong> :</p>

<blockquote>
  <p><em>“However, a person who does not rely on G-d involves himself in the means of pursuing his livelihood because he relies on them for help and protection… If they do indeed help him, then he will praise them…</em></p>
</blockquote>

<blockquote>
  <p><em>If, however, they do not help him, then he will abandon them, reject them, and turn his desire away from them.”</em></p>
</blockquote>

<p>As I was reading this, it hit me: <strong>This is exactly the concept of Decoupling in programming.</strong></p>

<h3 id="spaghetti-code-vs-modulardesign">Spaghetti Code vs. Modular Design</h3>

<p>As developers, we know the pain of “tightly coupled” code. That’s when Module A is so knowledgeable about, and dependent on, the inner workings of Module B that you can’t touch one without breaking the other. If you want to swap out your database from MySQL to Postgres, but your business logic is writing raw SQL queries directly inside the controller, you’re in for a nightmare. Everything is tangled. The logic <em>depends</em> on the specific implementation.</p>

<p>Good architecture, on the other hand, strives for <strong>Decoupling</strong>.</p>

<p>You define an interface. Your business logic requests data, but it doesn’t care <em>how</em> that data is retrieved. You can swap out the database, change the API, or refactor the entire backend — and as long as the interface remains the same, the application keeps running smoothly. The logic is independent of the implementation details.</p>

<h3 id="refactoring-yourbitachon">Refactoring Your Bitachon</h3>

<p><em>Shaar HaBitachon</em> is teaching us to refactor our lives to be <strong>loosely coupled</strong>.</p>

<p>A person with true Bitachon has “decoupled” their livelihood (<em>Parnassah</em>) from their job.</p>

<ul>
  <li><strong>The Interface:</strong> G-d’s promise to sustain us.</li>
  <li><strong>The Implementation:</strong> The current job, gig, or business deal (the means/effort).</li>
</ul>

<p>When you are tightly coupled to your job, you are living in a legacy codebase full of dependencies. You think, “This job is the <em>only</em> way I can pay my mortgage.” That’s a fragile architecture. If that specific “module” (the job) crashes, your whole system goes down.</p>

<p>However, when you live with Bitachon, you realize that your livelihood comes from the “Sustenance Service” (G-d), and your job is just one interchangeable module used to deliver it.</p>

<p>If you lose your job, or a deal falls through? It’s not a system failure. It’s just a hot-swap. G-d is simply deprecating one method and initializing another. You don’t panic because your “Sustenance Provider” hasn’t changed — only the delivery mechanism has.</p>

<h3 id="the-whymatters">The “Why” Matters</h3>

<p>What really struck me in that chapter of <em>Shaar HaBitachon</em> is the motivation. The person with Bitachon still works just as hard! But why?</p>

<p>Not to <em>get</em> money, but to <strong>fulfill the will of G-d</strong>.</p>

<p>Just like we write modular code to make our applications robust, scalable, and maintainable, we should strive to make our trust in G-d robust and modular. We do the work because it’s the right thing to do (the spec), but we know that the result is handled entirely by the Core System.</p>

<p>So, the next time you’re refactoring a messy class or abstracting away a dependency, take a second to think: <strong>Is my own Bitachon tightly coupled to my job, or is it modular enough to handle whatever life throws at it?</strong></p>

<p><img src="/assets/images/posts/2025-12-22-2025-12-22_Refactoring-Your-Bitachon--Moving-From-Monolith-to-Modular-0.jpeg" alt="My Sha’ar Habitachon on my desk, in the background you can see my laptop as well as a few stickers strewn about on the desk." /></p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="" /><category term="" /><category term="torah-tech" /><category term="writing" /><category term="reflections" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Do You Even Wordle?</title><link href="https://blog.yechiel.me/do-you-even-wordle-e32de7da25fd" rel="alternate" type="text/html" title="Do You Even Wordle?" /><published>2022-01-27T21:56:58-05:00</published><updated>2022-01-27T21:56:58-05:00</updated><id>https://blog.yechiel.me/2022-01-28_Do-You-Even-Wordle-</id><content type="html" xml:base="https://blog.yechiel.me/do-you-even-wordle-e32de7da25fd"><![CDATA[<hr />

<h3 id="do-you-evenwordle">Do You Even Wordle?</h3>

<blockquote>

</blockquote>

<p>If you’re on social media, chances are that you are one of two kinds of people; those who post their Wordle results every day or those who are extremely annoyed by the yellow and green boxes flooding their feeds.</p>

<p>What is it about this word game that took the world by storm and made it so popular (at least until the next trend comes along)?</p>

<p>There are plenty of takes out there, but it seems to me that people find Wordle so wholesome because it stands in contrast to so much of what makes the toxic parts of the internet so toxic.</p>

<p>Unlike most apps and games out there, Wordle isn’t spying on you, it isn’t trying to drive engagement, it’s not addictive, and it isn’t trying to suck you into playing for hours every day. In fact, by only giving one word a day, it sets a pretty tight limit to how much time you can waste on it every day.</p>

<p>A lot has been said about the toxic parts of internet culture, and the large tech companies have been blamed for putting shareholder profits over the well-being of their users.</p>

<p>In Parshat Mishpatim, the first Parshah after the Jews got the Torah at Mount Sinai, the Torah goes into great detail about the laws of civil liability and the responsibility we have for damages caused by our actions and our property.</p>

<p>Just as a person can’t say “I just let my goat graze; it’s not my fault she ate up your tomato patch,” similarly, we can’t absolve ourselves from responsibility for the products and the technology we create.</p>

<p>Sure, profits are important, but they can’t come at the expense of the first principle; don’t be evil.</p>

<p>P.S. Really? “Knoll”? What’s up with <strong>that</strong>?</p>

<p><img src="/assets/images/posts/2022-01-28-2022-01-28_Do-You-Even-Wordle--0.jpg" alt="A screenshot of wordle results." /></p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="" /><category term="" /><category term="torah-tech" /><category term="reflections" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Talmudic Gems For Rails Developers</title><link href="https://blog.yechiel.me/talmudic-gems-for-rails-developers-77f6cb98c9ad" rel="alternate" type="text/html" title="Talmudic Gems For Rails Developers" /><published>2021-05-19T21:40:59-04:00</published><updated>2021-05-19T21:40:59-04:00</updated><id>https://blog.yechiel.me/2021-05-20_Talmudic-Gems-For-Rails-Developers</id><content type="html" xml:base="https://blog.yechiel.me/talmudic-gems-for-rails-developers-77f6cb98c9ad"><![CDATA[<hr />

<h3 id="talmudic-gems-for-rails-developers">Talmudic Gems For Rails Developers</h3>

<p><img src="/assets/images/posts/2021-05-20-2021-05-20_Talmudic-Gems-For-Rails-Developers-0.jpg" alt="A page from the talmud." /></p>

<p>A few weeks ago, I gave a talk at RailsConf titled Talmudic Gems For Rails Developers.</p>

<p>In the talk I discussed lessons I learned from a lifetime of Talmudic study that helped me in my journey as a developer, and which I felt could benefit other developers in their growth.</p>

<p>The talk is geared at any developer looking to grow using the timeless wisdom of the Talmudic sages, not just Rails developers.</p>

<p>I shared a transcript of the talk in the Torah &amp;&amp; Tech newsletter <a href="https://mailchi.mp/2bc93e8bd182/torah-tech-issue-6751637?e=bb66b91b8e">issue #123</a> (if you haven’t yet, you can sign up to the newsletter, and get the first year’s worth of newsletters in book format, at the Torah &amp;&amp; Tech website: <a href="https://torahandtech.dev/">torahandtech.dev</a>).</p>

<p>The talk has just been posted to YouTube, and I’m happy to share it with my audience!</p>

<iframe src="https://www.youtube.com/embed/d08GFQDT824?feature=oembed" width="700" height="393" frameborder="0" scrolling="no"></iframe>

<p>If you would like to delve into some of the sources I mentioned, you can find links in the source sheet I prepared <a href="https://gist.github.com/achasveachas/000e4e08e193140e372cd4819d025f4a">here</a>.</p>

<p>Happy Learning!</p>]]></content><author><name>Yechiel Kalmenson</name></author><category term="" /><category term="" /><category term="rails" /><category term="torah-tech" /><category term="writing" /><summary type="html"><![CDATA[]]></summary></entry></feed>