<?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: Vagebond</title>
    <description>The latest articles on DEV Community by Vagebond (@vagebond).</description>
    <link>https://dev.to/vagebond</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%2F4218081%2F1c2ab1c2-d732-43cc-83c6-7fadfc7794a0.jpeg</url>
      <title>DEV Community: Vagebond</title>
      <link>https://dev.to/vagebond</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vagebond"/>
    <language>en</language>
    <item>
      <title>Your keys are in plain files. Move them into the vault.</title>
      <dc:creator>Vagebond</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:29:01 +0000</pubDate>
      <link>https://dev.to/vagebond/your-keys-are-in-plain-files-move-them-into-the-vault-jlo</link>
      <guid>https://dev.to/vagebond/your-keys-are-in-plain-files-move-them-into-the-vault-jlo</guid>
      <description>&lt;p&gt;In the last post we showed that ignore rules and deny rules leave gaps. An agent that can read your home folder can read what lies there.&lt;/p&gt;

&lt;p&gt;This is the follow-up. Find those files. Then move them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: look for yourself
&lt;/h2&gt;

&lt;p&gt;Start with a few plain commands. No tool needed.&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;ls&lt;/span&gt; ~/.ssh
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.aws/credentials
find ~ &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="nt"&gt;-not&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/node_modules/*"&lt;/span&gt; 2&amp;gt;/dev/null
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"BEGIN OPENSSH PRIVATE KEY"&lt;/span&gt; ~ 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line that prints something is a secret in a plain file. Any process that runs as you can read it. That includes an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: run the import scan
&lt;/h2&gt;

&lt;p&gt;Kluis has an import scan. It finds keys, passwords, seed phrases and KeePass databases in your files.&lt;/p&gt;

&lt;p&gt;You see the list. You choose what goes into the vault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: move them into the vault
&lt;/h2&gt;

&lt;p&gt;Each secret becomes an entry. Every entry is an age file, so the format is open. Nothing is locked into Kluis.&lt;/p&gt;

&lt;p&gt;Keys are kept in the Secure Enclave and never leave the Mac. Every read of a secret asks for Touch ID. For long work, a timed session starts after one Touch ID. An audit log records the reads.&lt;/p&gt;

&lt;p&gt;Once an entry is in the vault, you can delete the plain copy yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: give a secret to one command
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kluis run&lt;/code&gt; gives a secret to one command. When the command ends, the secret is gone.&lt;/p&gt;

&lt;p&gt;An agent can use a secret without reading it, limited by a policy. The key goes to the command, never back to the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kluis is not
&lt;/h2&gt;

&lt;p&gt;Kluis is a password vault for the Mac. It needs macOS 14 or later on Apple silicon. There is no account and no server. It is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Download the disk image, drag Kluis to Applications and open it. Setup takes one minute, with no account.&lt;/p&gt;

&lt;p&gt;Then run the scan and see what lies in your home folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://baken.vagebond.nl/r/HfNztE2d" rel="noopener noreferrer"&gt;https://baken.vagebond.nl/r/HfNztE2d&lt;/a&gt;&lt;/p&gt;

</description>
      <category>bash</category>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>security</category>
    </item>
    <item>
      <title>Your agent reads .env. It can't read a vault.</title>
      <dc:creator>Vagebond</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:27:06 +0000</pubDate>
      <link>https://dev.to/vagebond/your-agent-reads-env-it-cant-read-a-vault-4k2j</link>
      <guid>https://dev.to/vagebond/your-agent-reads-env-it-cant-read-a-vault-4k2j</guid>
      <description>&lt;p&gt;You give an AI agent a shell in your project. The project has a &lt;code&gt;.env&lt;/code&gt; file. The agent can read it.&lt;/p&gt;

&lt;p&gt;Most advice offers two fixes: an ignore file and deny rules. Both help. Both leave gaps. Here is where, with commands you can try.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ignore file
&lt;/h2&gt;

&lt;p&gt;An ignore file only works for the tools that read it. It can hide a file from search. It does not stop a shell command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent can run commands, it can print the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deny rules
&lt;/h2&gt;

&lt;p&gt;Claude Code lets you deny tools in &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(cat:*)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now try these:&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;head&lt;/span&gt; .env
&lt;span class="nb"&gt;grep &lt;/span&gt;KEY .env
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; p .env
python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"print(open('.env').read())"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each rule names one path or one command. The list of other ways to read a file is long.&lt;/p&gt;

&lt;h2&gt;
  
  
  The environment
&lt;/h2&gt;

&lt;p&gt;Many keys never sit in the repo. You export them in your shell profile:&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;export &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent starts from that shell. Every child process inherits the variable.&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;printenv
echo&lt;/span&gt; &lt;span class="nv"&gt;$OPENAI_API_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No ignore file or deny rule covers that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settings that help
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep &lt;code&gt;.env&lt;/code&gt; out of the folder you open the agent in.&lt;/li&gt;
&lt;li&gt;Deny &lt;code&gt;Read&lt;/code&gt; on secret paths, as above.&lt;/li&gt;
&lt;li&gt;Keep Bash on ask, not allow.&lt;/li&gt;
&lt;li&gt;Don't export keys in &lt;code&gt;~/.zshrc&lt;/code&gt;. Start the agent from a clean shell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These shrink the gap. They don't close it. As long as the secret is in a file or in the environment, the agent can reach it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One fix: give the key to the command
&lt;/h2&gt;

&lt;p&gt;Kluis is a password vault for the Mac, for the terminal and the desktop. Your secrets live in the vault, not in plain files.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;kluis run&lt;/code&gt; gives a secret to one command. The secret is gone when that command ends.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent asks &lt;code&gt;kluis run&lt;/code&gt; to start a command with a secret.&lt;/li&gt;
&lt;li&gt;Touch ID confirms the read.&lt;/li&gt;
&lt;li&gt;The command gets the secret. The agent gets the output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key goes to the command, never back to the agent. The agent can use a secret without reading it. A policy limits what it can use. Every read goes into an audit log.&lt;/p&gt;

&lt;p&gt;To move what is already lying around, an import scan finds keys and passwords in your files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kluis is not
&lt;/h2&gt;

&lt;p&gt;Kluis does not sandbox your agent. A secret you leave in a plain file stays readable. Kluis helps once the secret is in the vault.&lt;/p&gt;

&lt;p&gt;It requires macOS 14 or later on Apple silicon. It is free. No account, no server.&lt;/p&gt;

&lt;p&gt;The docs and the download are on the Kluis site:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://baken.vagebond.nl/r/TbrWtFJg" rel="noopener noreferrer"&gt;https://baken.vagebond.nl/r/TbrWtFJg&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>Touch ID for sudo over ssh. Without a PAM module.</title>
      <dc:creator>Vagebond</dc:creator>
      <pubDate>Sat, 31 Oct 2026 01:04:08 +0000</pubDate>
      <link>https://dev.to/vagebond/touch-id-for-sudo-over-ssh-without-a-pam-module-59gm</link>
      <guid>https://dev.to/vagebond/touch-id-for-sudo-over-ssh-without-a-pam-module-59gm</guid>
      <description>&lt;p&gt;On your Mac, sudo can ask for Touch ID. Over ssh, it cannot. This post walks through the gap, the usual fix and another way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Touch ID for sudo, locally
&lt;/h2&gt;

&lt;p&gt;Since macOS 14, one line in one file is enough. The file survives system updates.&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;sudo cp&lt;/span&gt; /etc/pam.d/sudo_local.template /etc/pam.d/sudo_local
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/pam.d/sudo_local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uncomment this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;auth&lt;/span&gt;       &lt;span class="n"&gt;sufficient&lt;/span&gt;     &lt;span class="n"&gt;pam_tid&lt;/span&gt;.&lt;span class="n"&gt;so&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;sudo&lt;/code&gt; in Terminal asks for your fingerprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it stops at ssh
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pam_tid.so&lt;/code&gt; runs on your Mac. When you run sudo on a server, PAM runs on the server. The server has no Touch ID sensor. So it asks for a password.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual fix: a PAM module on every server
&lt;/h2&gt;

&lt;p&gt;The common route is an ssh key that needs Touch ID, plus &lt;code&gt;pam_ssh_agent_auth&lt;/code&gt; on the server. sudo then checks your forwarded agent instead of a password.&lt;/p&gt;

&lt;p&gt;On each server, install the module:&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;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libpam-ssh-agent-auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;/etc/pam.d/sudo&lt;/code&gt;, above the other auth lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="n"&gt;sufficient&lt;/span&gt; &lt;span class="n"&gt;pam_ssh_agent_auth&lt;/span&gt;.&lt;span class="n"&gt;so&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;=/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;security&lt;/span&gt;/&lt;span class="n"&gt;authorized_keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In sudoers, through &lt;code&gt;sudo visudo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight puppet"&gt;&lt;code&gt;&lt;span class="nc"&gt;Defaults&lt;/span&gt; &lt;span class="n"&gt;env_keep&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;"SSH_AUTH_SOCK"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then connect with agent forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-A&lt;/span&gt; you@server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. It also means a PAM change on every server. The list of keys has to stay in sync. And the server can use your agent socket for as long as you are connected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Another way: type the password
&lt;/h2&gt;

&lt;p&gt;Kluis is a password vault for the Mac, for the terminal and the desktop. The &lt;code&gt;kluis&lt;/code&gt; command types sudo passwords over ssh.&lt;/p&gt;

&lt;p&gt;The server keeps its normal sudo and its normal password. You install nothing there.&lt;/p&gt;

&lt;p&gt;The password is not on the screen, not in the shell history, not in a file. Every read of a secret asks for Touch ID. Keys are kept in the Secure Enclave and never leave the Mac.&lt;/p&gt;

&lt;p&gt;A few things help with many servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timed sessions after one Touch ID.&lt;/li&gt;
&lt;li&gt;Rotate a sudo password on the server from the vault.&lt;/li&gt;
&lt;li&gt;An audit log of reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Kluis is not
&lt;/h2&gt;

&lt;p&gt;Kluis does not put Touch ID on the server. The server still asks for a password. Kluis types it for you.&lt;/p&gt;

&lt;p&gt;If you want sudo without any password, backed by a key, the PAM route is the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Kluis is free. It needs macOS 14 or later on Apple silicon. No account, no server.&lt;/p&gt;

&lt;p&gt;Download the disk image, drag Kluis to Applications and open it. The site links to the docs for the &lt;code&gt;kluis&lt;/code&gt; commands.&lt;/p&gt;

&lt;p&gt;Kluis is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://baken.vagebond.nl/r/ryHBvrA9" rel="noopener noreferrer"&gt;https://baken.vagebond.nl/r/ryHBvrA9&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>From Inertia::render() to a page type</title>
      <dc:creator>Vagebond</dc:creator>
      <pubDate>Sat, 31 Oct 2026 00:38:22 +0000</pubDate>
      <link>https://dev.to/vagebond/from-inertiarender-to-a-page-type-19l5</link>
      <guid>https://dev.to/vagebond/from-inertiarender-to-a-page-type-19l5</guid>
      <description>&lt;p&gt;You type your Inertia props by hand. Then a controller changes, and the types drift.&lt;/p&gt;

&lt;p&gt;Inertia Typegen generates TypeScript types and route helpers for Inertia pages. It reads them from Scramble's OpenAPI document. You write no types by hand.&lt;/p&gt;

&lt;p&gt;This post walks from the PHP input to the TypeScript output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require vagebond/inertia-typegen
composer require &lt;span class="nt"&gt;--dev&lt;/span&gt; dedoc/scramble
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scramble is a development dependency. Inertia Typegen requires PHP 8.3+, Laravel 12 or 13, and Scramble 0.13.49+. It also needs inertia-laravel 2 or 3, Node 22.18+ and Vite 6+.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PHP input
&lt;/h2&gt;

&lt;p&gt;Here is a controller with an enum and a paginated resource collection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="nc"&gt;PostStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;Published&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Posts/Index'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'posts'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;PostStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The page type
&lt;/h2&gt;

&lt;p&gt;Each Inertia::render() call becomes a page type. The output below is simplified for this post.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PostStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PostsIndexProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SharedData&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// the paginator keeps its real shape&lt;/span&gt;
  &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PostCollection&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PostStatus&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things happen here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHP enums become a union of their values.&lt;/li&gt;
&lt;li&gt;JSON resources, collections and paginators keep their real shape.&lt;/li&gt;
&lt;li&gt;Shared data from the Inertia middleware becomes one type that each page type includes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Form data
&lt;/h2&gt;

&lt;p&gt;Form request rules become the type of the form data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StorePostRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PostStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;StorePostData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PostStatus&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You change a rule in PHP. The form type follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route helpers
&lt;/h2&gt;

&lt;p&gt;You also get typed route helpers for each named route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping files fresh
&lt;/h2&gt;

&lt;p&gt;The Vite dev server regenerates files after a PHP change. A CI check fails when committed files are stale. You commit the generated files, so a production build needs no PHP.&lt;/p&gt;

&lt;p&gt;No HTTP endpoint is added by default. Plugins with hooks can change the document, routes and output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limit
&lt;/h2&gt;

&lt;p&gt;The release status is 0.x. Minor releases can contain breaking changes until 1.0. So pin the minor version and check each upgrade.&lt;/p&gt;

&lt;p&gt;Inertia Typegen is MIT licensed. The docs cover setup, route helpers and plugins:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://baken.vagebond.nl/r/dpRMA9Ze" rel="noopener noreferrer"&gt;https://baken.vagebond.nl/r/dpRMA9Ze&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
