<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: lef237</title>
    <description>The latest articles on DEV Community by lef237 (@lef237).</description>
    <link>https://dev.to/lef237</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3432083%2Fa7f2ce66-61e8-43cc-b1b6-e87e67fdf0e2.jpg</url>
      <title>DEV Community: lef237</title>
      <link>https://dev.to/lef237</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lef237"/>
    <language>en</language>
    <item>
      <title>Track Codex, Claude, and OpenCode Usage Limits on One Screen with costsee</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Sun, 02 Aug 2026 03:21:53 +0000</pubDate>
      <link>https://dev.to/lef237/bringing-coding-agent-usage-limits-together-on-one-screen-with-costsee-4c6l</link>
      <guid>https://dev.to/lef237/bringing-coding-agent-usage-limits-together-on-one-screen-with-costsee-4c6l</guid>
      <description>&lt;p&gt;Many people probably use all three of these tools: Codex, Claude Code, and OpenCode.&lt;/p&gt;

&lt;p&gt;For each tool, you need to check how much usage you have left today, how much of the five-hour window you have consumed, whether you still have room in your weekly limit, and whether you have exceeded your monthly budget.&lt;/p&gt;

&lt;p&gt;Although all of these are fundamentally the same kind of information—remaining usage capacity—they must be checked in different places.&lt;/p&gt;

&lt;p&gt;I created &lt;a href="https://github.com/lef237/costsee" rel="noopener noreferrer"&gt;costsee&lt;/a&gt; to solve this problem. It is a TUI application written in Rust that displays all of these usage limits together on a single terminal screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know the Most Constrained Limit the Moment You Launch It
&lt;/h2&gt;

&lt;p&gt;First, take a look at the actual interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; COSTSEE   most constrained Claude Code · 7 days  93%             updated 18:08

┌ Codex · team ──────────────────────── Codex App Server · updated 07/31 18:08 ┐
│        5 hours  ███░░░░░░░░░░░░░░░░░░   12%  left 2h 59m   reset 07-31 21:08 │
│         7 days  █████████░░░░░░░░░░░░   41%  left 4d 23h   reset 08-05 18:08 │
│  spend control  █░░░░░░░░░░░░░░░░░░░░    4%  left 19d 23h  reset 08-20 18:08 │
└──────────────────────────────────────────────────────────────────────────────┘
┌ Claude Code ────────────────── reported by Claude Code · updated 07/31 18:08 ┐
│        5 hours  ████████████████░░░░░   78%  left 59m      reset 07-31 19:08 │
│         7 days  ████████████████████░   93%  left 1d 23h   reset 08-02 18:08 │
└──────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I'll also show you a screenshot.&lt;/p&gt;

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

&lt;p&gt;The bar and percentage columns use fixed widths shared by every tool. This lets you compare Codex’s &lt;code&gt;7 days&lt;/code&gt; limit with Claude Code’s &lt;code&gt;7 days&lt;/code&gt; limit simply by moving your eyes horizontally across the screen.&lt;/p&gt;

&lt;p&gt;Even if each tool used a different width, you would still be able to read the individual numbers. However, you would no longer be able to compare them visually. Because the entire purpose of this tool is to view multiple tools side by side, consistent widths were a non-negotiable requirement.&lt;/p&gt;
&lt;h3&gt;
  
  
  Naming the Most Constrained Limit in the Header
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;most constrained Claude Code · 7 days 93%&lt;/code&gt; message at the top of the screen serves this purpose. Before you read through each block individually, it tells you which limit is most likely to be reached first.&lt;/p&gt;

&lt;p&gt;When the terminal is narrow, the message is shortened in stages according to the importance of the information. It first becomes &lt;code&gt;93% Claude Code · 7 days&lt;/code&gt;, and on an even narrower screen, simply &lt;code&gt;93%&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Changing Colors at Specific Thresholds
&lt;/h3&gt;

&lt;p&gt;When usage exceeds 70%, the display turns yellow. When it exceeds 90%, it turns red.&lt;/p&gt;

&lt;p&gt;The border around the entire block changes color, not just the individual row. This makes it possible to notice a problem as a change at the edge of your vision, even when you are not looking directly at the screen.&lt;/p&gt;
&lt;h3&gt;
  
  
  Showing the Time Remaining Until Reset
&lt;/h3&gt;

&lt;p&gt;If the interface displayed only an absolute timestamp such as &lt;code&gt;reset 08-02 18:08&lt;/code&gt;, the reader would need to calculate the difference from the current time mentally.&lt;/p&gt;

&lt;p&gt;For that reason, costsee also displays the remaining time, such as &lt;code&gt;left 1d 23h&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When deciding whether to continue working, the information you actually need is not the reset time itself, but how much time remains until the reset occurs.&lt;/p&gt;
&lt;h2&gt;
  
  
  It Also Works as a Claude Code Status Line
&lt;/h2&gt;

&lt;p&gt;One somewhat unusual aspect of costsee is that it can run as Claude Code’s status line while also feeding the data it receives there into its own dashboard.&lt;/p&gt;

&lt;p&gt;You can connect it with the following command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;costsee setup
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This adds a &lt;code&gt;statusLine&lt;/code&gt; entry to &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. From that point onward, Claude Code calls costsee for every message, and costsee returns the following two lines:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Opus[high] · costsee(main) · ctx ███░░░░░░░  28% · $1.23
5h ████████░░  78% left 58m · 7d █████████░  93% left 1d 22h
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The first line shows the state of the current session: the model name and its configuration, the working directory and branch, context consumption, and cost.&lt;/p&gt;

&lt;p&gt;The second line shows the subscription usage limits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6xwutauccswubiog0zk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6xwutauccswubiog0zk5.png" alt="Customize the status line" width="799" height="149"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Existing Status Lines Are Never Overwritten
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;statusLine&lt;/code&gt; is already configured, costsee exits with an error without modifying the existing settings in any way.&lt;/p&gt;

&lt;p&gt;It then explains how to add &lt;code&gt;costsee capture-claude&lt;/code&gt; to the existing script and pass standard input to it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Values Update Without Interrupting Your Work
&lt;/h2&gt;

&lt;p&gt;Because the cache is updated whenever you interact with Claude Code, costsee monitors the modification time of that file.&lt;/p&gt;

&lt;p&gt;When it detects a change, it automatically retrieves the latest data and displays &lt;code&gt;✓ auto-refreshed&lt;/code&gt; in the footer.&lt;/p&gt;

&lt;p&gt;Leave costsee open in an adjacent pane, and the values will continue to update simply as you use Claude Code.&lt;/p&gt;

&lt;p&gt;To refresh manually, press &lt;code&gt;r&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One detail here is that communication with the Codex App Server can sometimes take time. For this reason, costsee draws &lt;code&gt;refreshing…&lt;/code&gt; before it begins collecting data.&lt;/p&gt;

&lt;p&gt;This avoids leaving the user in front of an apparently unresponsive screen without any indication that processing is underway.&lt;/p&gt;

&lt;p&gt;When the refresh completes, &lt;code&gt;✓ refreshed&lt;/code&gt; appears for three seconds and then quietly disappears.&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;q&lt;/code&gt; or &lt;code&gt;Esc&lt;/code&gt; to exit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsnhr8gnm54dxz4l1ea8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsnhr8gnm54dxz4l1ea8i.png" alt="refreshed" width="518" height="144"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Using It from Scripts
&lt;/h2&gt;

&lt;p&gt;There are also situations where you want to retrieve only the numbers without opening the TUI.&lt;/p&gt;

&lt;p&gt;In that case, use the following command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;costsee report
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tool        window   used   reset                       status
Codex       7 days   2.0%   2026-08-05T20:59:27+09:00  ok
Claude Code 5 hours  23.5%  2026-07-29T22:00:00+09:00  ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Privacy Is a Design Requirement
&lt;/h2&gt;

&lt;p&gt;Because this tool handles usage information, I established clear boundaries from the beginning regarding the data it is allowed to access.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;Codex&lt;/strong&gt;, costsee temporarily starts the official App Server over stdio and reads only the result of &lt;code&gt;account/rateLimits/read&lt;/code&gt;. It does not read session logs or authentication credentials. Codex’s own authentication is used directly, so costsee never takes possession of an API key.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;OpenCode&lt;/strong&gt;, costsee opens the local SQLite database in read-only mode and reads only timestamps, providers, and costs. It never accesses conversation content.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;Claude Code&lt;/strong&gt;, costsee caches only usage percentages and reset times. The model name, directory, context usage, and cost shown in the status line are rendered directly from the input and are never written to disk. Session IDs and transcript paths are neither displayed nor stored.&lt;/li&gt;
&lt;li&gt;No telemetry is transmitted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One additional point regarding OpenCode is that there is currently no official API that can accurately aggregate usage by time window without reading conversation text.&lt;/p&gt;

&lt;p&gt;For this reason, access to the local database is isolated to the smallest possible scope. When costsee encounters an unexpected data format, it gives up rather than producing an estimated value.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;Codex is detected without any additional configuration.&lt;/p&gt;

&lt;p&gt;OpenCode, however, supports multiple providers and therefore does not have the concept of a single shared usage limit. You must configure the spending limits yourself.&lt;/p&gt;

&lt;p&gt;First, generate the configuration file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;costsee init
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then enter the official OpenCode Go limits in the generated &lt;code&gt;~/.config/costsee/config.toml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[allowances.opencode_go]&lt;/span&gt;
&lt;span class="py"&gt;five_hour_usd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;12.0&lt;/span&gt;
&lt;span class="py"&gt;weekly_usd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;
&lt;span class="py"&gt;monthly_usd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;60.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The configuration-file path is standardized as &lt;code&gt;~/.config/costsee/config.toml&lt;/code&gt; on every operating system.&lt;/p&gt;

&lt;p&gt;If the location varied between operating systems, it would create difficulties for people who share dotfiles across multiple environments.&lt;/p&gt;

&lt;p&gt;You can also use the &lt;code&gt;--config&lt;/code&gt; option to specify any path you prefer.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;codex&lt;/code&gt; is not included in your &lt;code&gt;PATH&lt;/code&gt;, you can specify the executable path as follows:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[commands]&lt;/span&gt;
&lt;span class="py"&gt;codex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/path/to/codex"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;One point to be aware of is that the exact reset times for the five-hour and weekly OpenCode Go limits cannot be determined from local history.&lt;/p&gt;

&lt;p&gt;For those two limits, costsee displays &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here again, the policy is to state clearly when something is unknown.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Building costsee requires Rust 1.89 or later because it uses &lt;code&gt;std::fs::File::lock&lt;/code&gt; when updating the Claude Code cache.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;cargo install costsee
costsee
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The main dependencies include ratatui 0.30, crossterm, rusqlite with the &lt;code&gt;bundled&lt;/code&gt; feature, clap, and chrono.&lt;/p&gt;

&lt;p&gt;At approximately 2,600 lines of source code, it is a relatively small tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The biggest change since I started keeping this tool open is that I now spend almost no time worrying about usage limits.&lt;/p&gt;

&lt;p&gt;Previously, I would check three separate locations whenever I felt uneasy, but now I can simply verify the information on one terminal screen.&lt;/p&gt;

&lt;p&gt;If the display has turned red after exceeding 90%, I can decide within seconds to postpone a resource-intensive task until the following day.&lt;/p&gt;

&lt;p&gt;The repository is available below. Feedback and issue reports after trying it are welcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lef237/costsee" rel="noopener noreferrer"&gt;https://github.com/lef237/costsee&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crates.io/crates/costsee" rel="noopener noreferrer"&gt;https://crates.io/crates/costsee&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/costsee" rel="noopener noreferrer"&gt;
        costsee
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;costsee&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/lef237/costsee/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/lef237/costsee/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A Rust TUI that shows how much of your Codex, Claude Code, and OpenCode
allowances you have used—all in one place.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt; COSTSEE   most constrained Claude Code · 7 days  93%             updated 18:08
┌ Codex · team ──────────────────────── Codex App Server · updated 07/31 18:08 ┐
│        5 hours  ███░░░░░░░░░░░░░░░░░░   12%  left 2h 59m   reset 07-31 21:08 │
│         7 days  █████████░░░░░░░░░░░░   41%  left 4d 23h   reset 08-05 18:08 │
│  spend control  █░░░░░░░░░░░░░░░░░░░░    4%  left 19d 23h  reset 08-20 18:08 │
└──────────────────────────────────────────────────────────────────────────────┘
┌ Claude Code ────────────────── reported by Claude Code · updated 07/31 18:08 ┐
│        5 hours  ████████████████░░░░░   78%  left 59m      reset 07-31 19:08 │
│         7 days  ████████████████████░   93%  left 1d 23h   reset 08-02 18:08 │
└──────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Bar widths are consistent across tools, so usage can be compared directly
between blocks. Borders turn yellow above 70% usage and red above 90%.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Supported allowances&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Codex: usage percentages for…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/costsee" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>codex</category>
      <category>claude</category>
      <category>opencode</category>
      <category>tui</category>
    </item>
    <item>
      <title>Use gst to safely get an overview of all git states</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Thu, 04 Jun 2026 11:56:32 +0000</pubDate>
      <link>https://dev.to/lef237/use-gst-for-an-overview-of-every-git-state-35km</link>
      <guid>https://dev.to/lef237/use-gst-for-an-overview-of-every-git-state-35km</guid>
      <description>&lt;p&gt;Checking changed files with &lt;code&gt;git status&lt;/code&gt;, looking at history with &lt;code&gt;git log&lt;/code&gt;, checking diffs with &lt;code&gt;git diff&lt;/code&gt;...&lt;/p&gt;

&lt;p&gt;When you repeatedly jump back and forth between these commands while working, you sometimes start thinking, “It would be nice if I could see all the information I want right now in one place.”&lt;/p&gt;

&lt;p&gt;So I created gst, a CLI tool that lets you check the state of a Git repository at a glance.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lef237/gst" rel="noopener noreferrer"&gt;lef237/gst: Read-only Git status visualizer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I was teaching Git to an acquaintance who had just started programming, and they said things like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;I don’t really understand git status&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;It’s hard to remember so many different commands&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’ve heard of tools like lazygit, but I’m scared I might accidentally change the repository state&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That made me think: why not build a tool that solves those problems?&lt;/p&gt;

&lt;p&gt;That was the starting point for this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can See
&lt;/h2&gt;

&lt;p&gt;When you launch &lt;code&gt;gst&lt;/code&gt;, you can view your current branch, differences from remote repositories, modified files, staged changes, working directory diffs, stashes, and the commit graph—all from your terminal.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Demo video
&lt;/h2&gt;

&lt;p&gt;A picture is worth a thousand words, so we have also prepared a video demonstration.&lt;/p&gt;

&lt;p&gt;Watching it will give you a better sense of how &lt;code&gt;gst&lt;/code&gt; works in practice and why it can be useful.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/EMO3DaNkqT0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;There are two ways to install it.&lt;/p&gt;

&lt;p&gt;If you use Go, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/lef237/gst/cmd/gst@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It also supports Homebrew. You can install it with:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;lef237/tap/gst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Normally, run the following command inside a repository:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you want to display the information once and then exit, use --once:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gst &lt;span class="nt"&gt;--once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In the TUI, you can move between views using the Tab key and arrow keys.&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;r&lt;/code&gt; to reload, and &lt;code&gt;q&lt;/code&gt; to quit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read-Only
&lt;/h2&gt;

&lt;p&gt;gst is a tool for checking the state of a repository.&lt;/p&gt;

&lt;p&gt;It does not perform operations that modify the repository state, such as &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, &lt;code&gt;checkout&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;merge&lt;/code&gt;, or &lt;code&gt;rebase.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Even if you are not very familiar with Git, there is less risk of accidentally changing something, so I think it is easy to use as a tool for checking repository status.&lt;/p&gt;
&lt;h2&gt;
  
  
  Diff display
&lt;/h2&gt;

&lt;p&gt;In Git, staged changes and unstaged changes in the working tree are managed separately.&lt;/p&gt;

&lt;p&gt;In gst, changed files are displayed with markers such as &lt;code&gt;INDEX&lt;/code&gt;, &lt;code&gt;WORKTREE&lt;/code&gt;, &lt;code&gt;NEW&lt;/code&gt;, and &lt;code&gt;CONFLICT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This makes it easier to understand which files are staged and which files still only have changes in the working tree.&lt;/p&gt;

&lt;p&gt;In the diff view, you can switch between the working tree diff and the staged diff.&lt;/p&gt;

&lt;p&gt;This is useful when you want to check what will be included in the next commit, or when you want to organize your changes before splitting them into separate commits.&lt;/p&gt;
&lt;h2&gt;
  
  
  Copying diffs
&lt;/h2&gt;

&lt;p&gt;In the diff view, you can copy the current diff directly to the clipboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4758g2l888o2k2fs24jk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4758g2l888o2k2fs24jk.png" alt="yank diff from gst" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;y&lt;/code&gt; to copy working directory diffs, and &lt;code&gt;i&lt;/code&gt; to copy staged diffs.&lt;/p&gt;

&lt;p&gt;Pressing &lt;code&gt;a&lt;/code&gt; copies a patch that includes all changes from &lt;code&gt;HEAD&lt;/code&gt; to the current working directory.&lt;/p&gt;

&lt;p&gt;The copied content is in patch format, so it can be applied with &lt;code&gt;git apply&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I think this is very useful!&lt;/p&gt;
&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;gst&lt;/strong&gt; is not a tool intended to replace Git operations.&lt;/p&gt;

&lt;p&gt;Rather, it is a tool for checking “what state is this repository in right now?” before you perform an operation.&lt;/p&gt;

&lt;p&gt;Please give it a try :)&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/gst" rel="noopener noreferrer"&gt;
        gst
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Read-only Git status visualizer
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;gst&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;code&gt;gst&lt;/code&gt; is a read-only Git status visualizer for people who want to understand the
shape of a repository before they run Git commands.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://private-user-images.githubusercontent.com/93074851/600953240-09fd03cf-2781-4d9f-b73d-f057e9e1ac90.jpg?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODE3ODYzMDEsIm5iZiI6MTc4MTc4NjAwMSwicGF0aCI6Ii85MzA3NDg1MS82MDA5NTMyNDAtMDlmZDAzY2YtMjc4MS00ZDlmLWI3M2QtZjA1N2U5ZTFhYzkwLmpwZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjE4VDEyMzMyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ0NjY3MzM5ZWE1NDhhMGU0NzhhNzdiZGE2MzYzZDJmMTQ5MjFhN2QwMjk2MzE5ODdkZjM5NTIxN2UwZmU1M2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmpwZWcifQ.DByZhVfdKbOF1Ei03GDBnQkbJw9kfhgxatWGDyJNuJU"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fprivate-user-images.githubusercontent.com%2F93074851%2F600953240-09fd03cf-2781-4d9f-b73d-f057e9e1ac90.jpg%3Fjwt%3DeyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODE3ODYzMDEsIm5iZiI6MTc4MTc4NjAwMSwicGF0aCI6Ii85MzA3NDg1MS82MDA5NTMyNDAtMDlmZDAzY2YtMjc4MS00ZDlmLWI3M2QtZjA1N2U5ZTFhYzkwLmpwZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MTglMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjE4VDEyMzMyMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ0NjY3MzM5ZWE1NDhhMGU0NzhhNzdiZGE2MzYzZDJmMTQ5MjFhN2QwMjk2MzE5ODdkZjM5NTIxN2UwZmU1M2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRmpwZWcifQ.DByZhVfdKbOF1Ei03GDBnQkbJw9kfhgxatWGDyJNuJU" alt="Image"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo: &lt;a href="https://youtu.be/EMO3DaNkqT0" rel="nofollow noopener noreferrer"&gt;https://youtu.be/EMO3DaNkqT0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Motivation&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Git beginners often struggle because the current state is split across several
places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;commits and branches form a graph&lt;/li&gt;
&lt;li&gt;local branches and remote tracking branches can point at different commits&lt;/li&gt;
&lt;li&gt;the index and working tree can each contain different file changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;gst&lt;/code&gt; puts those pieces into one terminal dashboard. It does not push, pull
checkout, commit, merge, rebase, or mutate the repository.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go install github.com/lef237/gst/cmd/gst@latest&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Or with Homebrew:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;brew install lef237/tap/gst&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;For local development:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go run ./cmd/gst&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;To build a local binary:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;go build -o tmp/gst ./cmd/gst&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Release steps are documented in &lt;a href="https://github.com/lef237/gst/docs/release.md" rel="noopener noreferrer"&gt;docs/release.md&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;gst
gst --interval 1s
gst --once
gst --no-color&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;By default, &lt;code&gt;gst&lt;/code&gt; opens the interactive TUI.&lt;/p&gt;
&lt;p&gt;Interactive controls:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Move between views with &lt;code&gt;tab&lt;/code&gt;, the left/right arrow keys…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/gst" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


</description>
      <category>git</category>
      <category>go</category>
      <category>cli</category>
      <category>terminal</category>
    </item>
    <item>
      <title>clauhist: browse full Claude Code history and resume sessions across projects</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Sat, 28 Mar 2026 04:43:38 +0000</pubDate>
      <link>https://dev.to/lef237/clauhist-browse-full-claude-code-history-and-resume-sessions-across-projects-1c1o</link>
      <guid>https://dev.to/lef237/clauhist-browse-full-claude-code-history-and-resume-sessions-across-projects-1c1o</guid>
      <description>&lt;p&gt;Claude Code can already help you resume work from a project if you move into that working directory and use &lt;code&gt;/resume&lt;/code&gt; there.&lt;/p&gt;

&lt;p&gt;The limitation is that this is tied to the current working directory. If you want to look back across all of your past work, including sessions from other repositories and directories, that gets awkward.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; is a small CLI tool for that case. It shows your Claude Code history in &lt;code&gt;fzf&lt;/code&gt;, lets you browse sessions across working directories, and resume one from the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it shows
&lt;/h2&gt;

&lt;p&gt;Sessions are sorted by recent activity. Each row includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the last activity time&lt;/li&gt;
&lt;li&gt;the project path&lt;/li&gt;
&lt;li&gt;whether the path still exists&lt;/li&gt;
&lt;li&gt;a preview of the first message&lt;/li&gt;
&lt;li&gt;the message count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a preview pane with the session ID, timestamps, and message list. Once you find the one you want, press &lt;code&gt;Enter&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; reads &lt;code&gt;~/.claude/history.jsonl&lt;/code&gt;, groups entries by session, and passes the result to &lt;code&gt;fzf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After you select a session, it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--resume&lt;/span&gt; &amp;lt;session-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It does not keep its own database or add another history layer. It is just a thin local browser over Claude Code's existing history file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;clauhist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or install from source:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lef237/clauhist.git
&lt;span class="nb"&gt;cd &lt;/span&gt;clauhist
cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You also need &lt;code&gt;fzf&lt;/code&gt; and Claude Code installed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clauhist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Main controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Enter&lt;/code&gt;: resume the selected session&lt;/li&gt;
&lt;li&gt;type: filter the list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl-/&lt;/code&gt;: toggle preview&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl-C&lt;/code&gt;: exit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is displayed on the terminal like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────╮
│ Claude Code History Browser  &lt;span class="o"&gt;[&lt;/span&gt;Enter: resume  Ctrl-/: toggle preview  Ctrl-C: cancel]  │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ Search:                                                                               │
│ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 2026-03-18 09:12  ✓ ~/projects/myapp      Tell me about Rust error handling…  &lt;span class="o"&gt;(&lt;/span&gt;12&lt;span class="o"&gt;)&lt;/span&gt;  │
│   2026-03-17 22:45  ✓ ~/sandbox/api-client  Generate client from OpenAPI schema  &lt;span class="o"&gt;(&lt;/span&gt;8&lt;span class="o"&gt;)&lt;/span&gt;  │
│   2026-03-17 14:30  ✗ ~/old-project         Database migration steps             &lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&lt;/span&gt;  │
╰───────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;✓&lt;/code&gt; means the project directory still exists. &lt;code&gt;✗&lt;/code&gt; means it was moved or deleted. You can still resume the session, but &lt;code&gt;cd&lt;/code&gt; into the original directory may fail.&lt;/p&gt;
&lt;h2&gt;
  
  
  Shell integration
&lt;/h2&gt;

&lt;p&gt;If you add this to your shell config:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;clauhist init zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;then &lt;code&gt;clauhist&lt;/code&gt; will now change to the project directory within your current shell before launching Claude Code.&lt;/p&gt;

&lt;p&gt;This allows you to immediately return to your original directory with &lt;code&gt;cd -&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without this, &lt;code&gt;clauhist&lt;/code&gt; uses a subshell for the directory change, so &lt;code&gt;exit&lt;/code&gt; to return to your original shell.&lt;/p&gt;
&lt;h2&gt;
  
  
  Local-only
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; only reads &lt;code&gt;~/.claude/history.jsonl&lt;/code&gt; and launches &lt;code&gt;claude --resume&lt;/code&gt;. It does not send your history anywhere.&lt;/p&gt;

&lt;p&gt;That is the whole tool: a simple way to browse past Claude Code sessions across projects and reopen one without moving directory by directory.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;You can find the repository here :)&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;
        clauhist
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Browse Claude Code history across working directories and resume sessions
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;clauhist&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Browse Claude Code history across working directories and resume sessions.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────╮
│ Claude Code History Browser  [Enter: resume  Ctrl-/: toggle preview  Ctrl-C: cancel]  │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ Search:                                                                               │
│ &amp;gt; 2026-03-18 09:12  ✓ ~/projects/myapp      Tell me about Rust error handling…  (12)  │
│   2026-03-17 22:45  ✓ ~/sandbox/api-client  Generate client from OpenAPI schema  (8)  │
│   2026-03-17 14:30  ✗ ~/old-project         Database migration steps             (3)  │
╰───────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;(Example output — actual appearance depends on your terminal and fzf version)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Claude Code can already resume work from the current project directory. clauhist solves a different problem: browsing the full history across all of your working directories from one place.&lt;/p&gt;
&lt;p&gt;Select a session and press &lt;code&gt;Enter&lt;/code&gt; — clauhist opens &lt;code&gt;claude --resume&lt;/code&gt; in the project directory. When you exit Claude, you return to your original shell.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why clauhist exists&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;If you &lt;code&gt;cd&lt;/code&gt; into a project and use Claude Code's &lt;code&gt;/resume&lt;/code&gt; there, you can inspect…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;a href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;https://github.com/lef237/clauhist&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>One-Time Editor, an editor for drafts before sending</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Mon, 09 Mar 2026 13:16:35 +0000</pubDate>
      <link>https://dev.to/lef237/one-time-editor-an-editor-for-drafts-before-sending-59gi</link>
      <guid>https://dev.to/lef237/one-time-editor-an-editor-for-drafts-before-sending-59gi</guid>
      <description>&lt;p&gt;I've created an app called "One-Time Editor".&lt;/p&gt;

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

&lt;p&gt;The image above shows dark mode. Here's what it looks like in light mode.&lt;/p&gt;

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

&lt;p&gt;What kind of app is it, you ask? It's a &lt;strong&gt;text editor that can be used for drafting messages before sending them in a chat&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By using this app, you can avoid the worry of accidentally sending a message prematurely by hitting Enter.&lt;/p&gt;

&lt;p&gt;To quickly understand how it works, I recommend watching the video, which I've attached below.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/qwj9fr77vQg"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="noopener noreferrer"&gt;https://youtu.be/qwj9fr77vQg&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Install
&lt;/h2&gt;

&lt;p&gt;For macOS, use Homebrew. You can start using it right away by typing the following commands in your terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap lef237/tap
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; one-time-editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Since it's an Electron-based app, it's also cross-platform compatible.&lt;/p&gt;

&lt;p&gt;Windows and Linux users can install the pre-built app from here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/lef237/one-time-editor/releases" rel="noopener noreferrer"&gt;https://github.com/lef237/one-time-editor/releases&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Windows users should select the .exe file, and Linux users should select the AppImage to install it. &lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;p&gt;The main highlight is that you can &lt;strong&gt;toggle (show/hide) the window using a keyboard shortcut&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What's more, &lt;strong&gt;it automatically copies the content you've entered as soon as you toggle it, allowing for instant pasting.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open One-Time Editor with a keyboard shortcut.&lt;/li&gt;
&lt;li&gt;Type the content you want to send.&lt;/li&gt;
&lt;li&gt;Close it with the keyboard shortcut.

&lt;ul&gt;
&lt;li&gt;The content will be automatically copied.&lt;/li&gt;
&lt;li&gt;On macOS, focus will automatically return to the window you were previously using.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Paste and then send.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also supports switching between light and dark modes. I think the design is also stylish.&lt;/p&gt;

&lt;p&gt;Of course, &lt;strong&gt;you can also customize the keyboard shortcut!&lt;/strong&gt; The default is Ctrl+J, but feel free to assign any shortcut you like.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I Made It
&lt;/h2&gt;

&lt;p&gt;Many people have probably experienced indecision about whether to use Enter, Shift+Enter, or Ctrl+Enter to send a chat message.&lt;/p&gt;

&lt;p&gt;And then, accidentally sending a message in the middle of writing...&lt;/p&gt;

&lt;p&gt;Using some kind of editor for drafting isn't bad, but it can be a bit cumbersome to constantly be asked to create a new file or to manage windows.&lt;/p&gt;

&lt;p&gt;This app is designed for throwaway writing, and it allows for displaying/hiding the window and copying content with a single shortcut, making it quite fast.&lt;/p&gt;
&lt;h2&gt;
  
  
  Challenges During Development
&lt;/h2&gt;

&lt;p&gt;There were several challenges.&lt;/p&gt;

&lt;p&gt;However, the most difficult problem was dealing with code signing and notarization.&lt;/p&gt;

&lt;p&gt;When you create an app with Electron, it works fine on your local machine, but if you upload it to the internet and download it, the app becomes corrupted.&lt;/p&gt;

&lt;p&gt;You might have seen this warning when downloading things like Docker:&lt;/p&gt;

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

&lt;p&gt;Therefore, you need to use the &lt;code&gt;xattr&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://osxhub.com/fix-macos-app-damaged-and-cant-be-opened-issue-guide/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.osxhub.com%2F2025%2F06%2Fmacos-app-damaged-and-cant-be-opend-issue" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://osxhub.com/fix-macos-app-damaged-and-cant-be-opened-issue-guide/" rel="noopener noreferrer" class="c-link"&gt;
            Fix macOS app is damaged and can't be opened" Error - Complete Guide - osxhub
          &lt;/a&gt;
        &lt;/h2&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.osxhub.com%2Fuploads%2F2025%2F06%2Ffavicon.png"&gt;
          osxhub.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;This means you need to run the command &lt;code&gt;xattr -rc "/Applications/hoge.app"&lt;/code&gt; after installation, which is a bit troublesome.&lt;/p&gt;

&lt;p&gt;When I looked into simplifying this, I realized that using &lt;code&gt;postflight&lt;/code&gt; could automate this process if installed via homebrew-tap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lef237/homebrew-tap/blob/main/Casks/one-time-editor.rb" rel="noopener noreferrer"&gt;https://github.com/lef237/homebrew-tap/blob/main/Casks/one-time-editor.rb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're looking to distribute Electron apps, please use this as a reference!&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;GitHub is also public.&lt;/p&gt;

&lt;p&gt;If you have any feature requests, please open an Issue. 🐘&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;
        one-time-editor
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A lightweight scratchpad that lives one shortcut away
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;One-Time Editor&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A lightweight scratchpad that lives one shortcut away. Draft a message, hit the shortcut again, and it's already on your clipboard — ready to paste anywhere.&lt;/p&gt;
&lt;p&gt;Built for the workflow of writing chat messages, AI prompts, and quick notes that you type once and send.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/831acaaa14e6e538238c6c9af7341628428f9638d6ebab57e7052245eaf3937b/68747470733a2f2f696d672e796f75747562652e636f6d2f76692f71776a39667237377651672f6d617872657364656661756c742e6a7067" alt="Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="nofollow noopener noreferrer"&gt;https://youtu.be/qwj9fr77vQg&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;macOS (Homebrew)&lt;/h3&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;brew tap lef237/tap
brew install --cask one-time-editor&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Manual download&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Pre-built binaries for macOS, Windows, and Linux are available on the &lt;a href="https://github.com/lef237/one-time-editor/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt; page.&lt;/p&gt;
&lt;p&gt;If you download manually on macOS, the app is not signed with an Apple Developer certificate, so macOS may show a warning. To allow it, run:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;xattr -cr &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;/Applications/One-Time Editor.app&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl+J&lt;/code&gt; to summon the editor&lt;/li&gt;
&lt;li&gt;Type your text&lt;/li&gt;
&lt;li&gt;Press the shortcut again — the window disappears and your text is copied to clipboard&lt;/li&gt;
&lt;li&gt;Paste wherever you need it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That's it. No save dialog, no file management, no friction.&lt;/p&gt;
&lt;p&gt;The shortcut is…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;&lt;a href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;https://github.com/lef237/one-time-editor&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;If it doesn't work on Windows or other OSs, please comment on GitHub!&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>productivity</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Writing a Domain Model in Ruby Without Using class</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Wed, 13 Aug 2025 10:47:28 +0000</pubDate>
      <link>https://dev.to/lef237/writing-a-domain-model-in-ruby-without-using-class-5h06</link>
      <guid>https://dev.to/lef237/writing-a-domain-model-in-ruby-without-using-class-5h06</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;From Ruby 3.2, the &lt;code&gt;Data&lt;/code&gt; class has been introduced, allowing us to explore ways of representing domain models without using &lt;code&gt;class&lt;/code&gt;. In this article, I’ll examine how we can do that.&lt;/p&gt;

&lt;p&gt;When writing business logic in Ruby, we naturally tend to use &lt;code&gt;class&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We set attributes in &lt;code&gt;initialize&lt;/code&gt;, and change state via instance methods. Especially if you’ve been writing Rails code, this feels natural.&lt;/p&gt;

&lt;p&gt;However, whether managing logic with a stateful class is suitable for &lt;em&gt;every&lt;/em&gt; case is debatable. For domain logic where we want to avoid side effects, class-based design might not always be optimal. In recent years, designs influenced by functional programming have gained attention, and as has been discussed on X (formerly Twitter), the mainstream approach in TypeScript is moving toward avoiding classes entirely.&lt;/p&gt;

&lt;p&gt;With the &lt;code&gt;Data&lt;/code&gt; class introduced in Ruby 3.2, we can represent domains while keeping structural data and logic separate. In this article, I’ll show you how.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This article focuses on &lt;em&gt;representing domain logic&lt;/em&gt;, separate from Rails' ActiveRecord models.&lt;br&gt;&lt;br&gt;
I’m specifically looking at a style that works purely with plain Ruby syntax, without being tied to the Rails environment. This is driven more by curiosity than by real-world constraints, so please keep that in mind.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is the &lt;code&gt;Data&lt;/code&gt; Class?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Data&lt;/code&gt; class introduced in Ruby 3.2 lets you define immutable data structures easily. It’s similar to the &lt;a href="https://docs.ruby-lang.org/en/master/Struct.html" rel="noopener noreferrer"&gt;Struct&lt;/a&gt; that Ruby already had, but all fields are immutable — to change a value, you must create a new instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.ruby-lang.org/en/master/Data.html" rel="noopener noreferrer"&gt;class Data - Documentation for Ruby&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To use a loose analogy: Struct is like a hash, while Data is like a read-only hash.&lt;/p&gt;

&lt;p&gt;Let’s actually design a domain model using the &lt;code&gt;Data&lt;/code&gt; class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Domain Model
&lt;/h2&gt;

&lt;p&gt;First, let’s think of an example.&lt;/p&gt;

&lt;p&gt;We’ll imagine a &lt;code&gt;Customer&lt;/code&gt; model with the following attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;first_name&lt;/code&gt; / &lt;code&gt;last_name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_active&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; / &lt;code&gt;updated_at&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We want to be able to perform operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changing the name&lt;/li&gt;
&lt;li&gt;Changing the email address&lt;/li&gt;
&lt;li&gt;Activating/deactivating the customer&lt;/li&gt;
&lt;li&gt;Getting the full name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially the first three operations would naturally become methods that change state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Class-Based Implementation
&lt;/h2&gt;

&lt;p&gt;First, let’s design it in the usual class-based way. You might find there are too many instance variables, among other nitpicks, but let’s just write it out as a sample.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;
    &lt;span class="vi"&gt;@email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
    &lt;span class="vi"&gt;@first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;
    &lt;span class="vi"&gt;@last_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
    &lt;span class="vi"&gt;@updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;
    &lt;span class="vi"&gt;@last_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;
    &lt;span class="n"&gt;touch&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
    &lt;span class="n"&gt;touch&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deactivate&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;touch&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;touch&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;full_name&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;touch&lt;/span&gt;
    &lt;span class="vi"&gt;@updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid name"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A[^@\s]+@[^@\s]+\z/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid email format"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be used as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Anderson"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Brown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deactivate&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_name&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "Bob Brown"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we create an instance with &lt;code&gt;new&lt;/code&gt; and call methods to change its state — a very common approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;Data&lt;/code&gt; Class Available from Ruby 3.2
&lt;/h2&gt;

&lt;p&gt;In Ruby 3.2, the &lt;code&gt;Data&lt;/code&gt; class was introduced.&lt;/p&gt;

&lt;p&gt;It feels similar to &lt;code&gt;Struct&lt;/code&gt;, but all fields are immutable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change a value, you use &lt;code&gt;.with&lt;/code&gt; to create a new instance.&lt;/p&gt;

&lt;p&gt;Suppose &lt;code&gt;customer1&lt;/code&gt; has &lt;code&gt;first_name&lt;/code&gt; set to &lt;code&gt;Bob&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="s2"&gt;"bob@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Johnson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;created_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change &lt;code&gt;first_name&lt;/code&gt; to &lt;code&gt;Carol&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Carol"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;customer1&lt;/code&gt; remains unchanged; &lt;code&gt;customer2&lt;/code&gt; is a new object with only &lt;code&gt;first_name&lt;/code&gt; different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;customer1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; "Bob"&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;customer2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; "Carol"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;Data.with&lt;/code&gt;, we handle state not by mutation but by replacement, avoiding unintended side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Domain Logic with &lt;code&gt;Data&lt;/code&gt; and Functions
&lt;/h2&gt;

&lt;p&gt;Let’s rewrite the earlier class-based code using &lt;code&gt;Data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We’ll define a &lt;code&gt;CustomerService&lt;/code&gt; module, separate from &lt;code&gt;Customer&lt;/code&gt;, to hold our domain operations as functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;CustomerService&lt;/span&gt;
  &lt;span class="kp"&gt;module_function&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
    &lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_active&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_active&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid name"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A[^@\s]+@[^@\s]+\z/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid email format"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;create_customer&lt;/code&gt; is used as a factory in &lt;code&gt;CustomerService&lt;/code&gt;. By defining domain operations as functions, we can handle changes to state in a way that minimizes side effects.&lt;/p&gt;

&lt;p&gt;Each function doesn’t change the &lt;code&gt;Customer&lt;/code&gt; data structure directly — it uses &lt;code&gt;.with&lt;/code&gt; to create a new instance. This preserves data immutability and brings the design closer to a functional style.&lt;/p&gt;

&lt;p&gt;Also, by using &lt;code&gt;module_function&lt;/code&gt;, we can call these functions without creating an instance of the module. Validation is performed inside the functions where needed.&lt;/p&gt;

&lt;p&gt;This cleanly separates data (&lt;code&gt;Customer&lt;/code&gt;) from behavior (functions).&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Anderson"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Brown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "Bob Brown"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that when reassigning, we return a &lt;em&gt;new&lt;/em&gt; object instead of modifying the original. I considered naming them &lt;code&gt;customer_updated&lt;/code&gt;, &lt;code&gt;customer_updated2&lt;/code&gt;, etc., but left it as-is for now.&lt;/p&gt;

&lt;p&gt;By not using instance variables and passing all state through arguments, we avoid side effects. At that point, all that’s left is to store the final state in the database or handle it however needed.&lt;/p&gt;

&lt;p&gt;For example, saving with SQLite might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"INSERT OR REPLACE INTO customers (id, email, first_name, last_name, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_active&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can access attributes with &lt;code&gt;customer.xxx&lt;/code&gt;, so it feels similar to using a class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Points to Note
&lt;/h2&gt;

&lt;p&gt;Of course, there’s nothing wrong with using &lt;code&gt;class&lt;/code&gt;. For UI logic, controllers, database models like ActiveRecord, and so on, class-based design often makes sense.&lt;/p&gt;

&lt;p&gt;In Rails projects, it’s common to extract structured data handling into POROs (Plain Old Ruby Objects) written as classes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/sulmanweb/plain-old-ruby-objects-poros-in-rails-fat-models-3l7f"&gt;Plain Old Ruby Objects (POROs) in Rails Fat Models - DEV Community&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, as domain logic grows more complex, tracking “state changes” through the code becomes harder. In such cases, a design where “values are immutable and logic is functional” — like the one shown here — can make maintenance easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Ruby is a flexible language, and you can represent domain logic without relying on classes or instance variables.&lt;/p&gt;

&lt;p&gt;Ruby 3.2’s &lt;code&gt;Data&lt;/code&gt; class can be a useful tool for designing the domain layer as structured data plus explicit functions. You don’t have to use it for every case, but simply knowing it’s an option can broaden your design possibilities.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ruby</category>
    </item>
  </channel>
</rss>
