<?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: Fernando Paladini</title>
    <description>The latest articles on DEV Community by Fernando Paladini (@paladini).</description>
    <link>https://dev.to/paladini</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%2F1065831%2F79b4d650-5838-4481-a62f-8f03f4010512.jpeg</url>
      <title>DEV Community: Fernando Paladini</title>
      <link>https://dev.to/paladini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paladini"/>
    <language>en</language>
    <item>
      <title>Build a Draft-First Medium Publishing Agent with MCP</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sun, 13 Sep 2026 12:39:30 +0000</pubDate>
      <link>https://dev.to/paladini/build-a-draft-first-medium-publishing-agent-with-mcp-3pe9</link>
      <guid>https://dev.to/paladini/build-a-draft-first-medium-publishing-agent-with-mcp-3pe9</guid>
      <description>&lt;p&gt;Publishing the same technical article on more than one platform sounds like a copy-and-paste task. In practice, the risky part is deciding what an AI agent is allowed to do with an authenticated browser session.&lt;/p&gt;

&lt;p&gt;This tutorial builds a draft-first Medium workflow with &lt;a href="https://github.com/paladini/publish-agents" rel="noopener noreferrer"&gt;publish-agents&lt;/a&gt;, an open-source TypeScript project by Fernando Paladini. The &lt;code&gt;@paladini/medium-publisher-mcp@&lt;/code&gt; package exposes Medium publishing operations through the &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;, while keeping login, import, review, and live publication as separate decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install the stable &lt;code&gt;v0.2.3&lt;/code&gt; package, log in once, register its stdio MCP server, and ask your MCP client to import a public DEV.to article with &lt;code&gt;publish: false&lt;/code&gt;. The agent can prepare and inspect a draft, but you retain approval for browser authentication, formatting changes, and publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;The resulting flow has four boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A public DEV.to article provides the source Markdown.&lt;/li&gt;
&lt;li&gt;The local MCP server exposes publishing tools to an AI client.&lt;/li&gt;
&lt;li&gt;Patchright uses a saved browser session to work in Medium.&lt;/li&gt;
&lt;li&gt;A human reviews the real editor before any live publication.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;MCP standardizes how a host connects to tools, but it does not make a tool safe by itself. The protocol documentation describes tools as capabilities that need consent and careful authorization. That is why this example uses a draft as the default outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or newer.&lt;/li&gt;
&lt;li&gt;An MCP-compatible client such as Cursor or Claude Code.&lt;/li&gt;
&lt;li&gt;A Medium account that can create stories.&lt;/li&gt;
&lt;li&gt;A published DEV.to article with a public URL.&lt;/li&gt;
&lt;li&gt;Permission to install the bundled Chromium browser locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tested release reference is &lt;a href="https://github.com/paladini/publish-agents/tree/v0.2.3/packages/medium-publisher" rel="noopener noreferrer"&gt;v0.2.3&lt;/a&gt;. The package declares &lt;code&gt;@paladini/medium-publisher-mcp&lt;/code&gt; version &lt;code&gt;0.2.3&lt;/code&gt;, requires Node.js &lt;code&gt;&amp;gt;=20&lt;/code&gt;, uses Patchright, and is licensed under MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the stable package
&lt;/h2&gt;

&lt;p&gt;The shortest path installs the published npm package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;paladini/medium-publisher-mcp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package exposes two commands: &lt;code&gt;medium-publisher&lt;/code&gt; for the CLI and &lt;code&gt;medium-publisher-mcp&lt;/code&gt; for the MCP server. Its post-install script requests the Patchright Chromium browser. If your package manager skipped that step, follow the &lt;a href="https://github.com/Kaliiiiiiiiii-Vinyzu/patchright" rel="noopener noreferrer"&gt;Patchright installation documentation&lt;/a&gt; before trying browser automation.&lt;/p&gt;

&lt;p&gt;You can verify that the CLI is on your PATH without logging in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer a source checkout, the stable repository documents this equivalent path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/paladini/publish-agents.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publish-agents&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v0.2.3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;paladini/medium-publisher-mcp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;paladini/medium-publisher-mcp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create the local browser session
&lt;/h2&gt;

&lt;p&gt;Login is interactive because the tool needs a Medium session, not a Medium API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The documented Windows default is &lt;code&gt;%LOCALAPPDATA%\\medium-publisher\\storageState.json&lt;/code&gt;. The package also supports &lt;code&gt;MEDIUM_STATE_PATH&lt;/code&gt; and &lt;code&gt;MEDIUM_PUBLISHER_HOME&lt;/code&gt; when you need a different local location.&lt;/p&gt;

&lt;p&gt;Check the session in structured output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;session-check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treat &lt;code&gt;storageState.json&lt;/code&gt; like a credential store. Do not commit it, attach it to an issue, or place it in a CI artifact. A local browser profile can contain cookies that grant access to your Medium account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Register the MCP server
&lt;/h2&gt;

&lt;p&gt;The server uses stdio. A minimal MCP configuration is:&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;"mcpServers"&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;"medium-publisher"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"medium-publisher-mcp"&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;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;The repository includes client-specific setup notes for &lt;a href="https://github.com/paladini/publish-agents/blob/v0.2.3/mcp/medium/README.md" rel="noopener noreferrer"&gt;Cursor and Claude Code&lt;/a&gt;. After registering the server, restart or reload the client so it discovers the tools.&lt;/p&gt;

&lt;p&gt;The package exposes seven tools in the documented release. The important ones for this workflow are &lt;code&gt;medium_session_check&lt;/code&gt;, &lt;code&gt;medium_publish_from_devto&lt;/code&gt;, &lt;code&gt;medium_extract&lt;/code&gt;, &lt;code&gt;medium_fix_draft&lt;/code&gt;, and &lt;code&gt;medium_open_draft&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import a DEV.to article as a draft
&lt;/h2&gt;

&lt;p&gt;Ask the MCP client to call the server with a public article URL and an explicit non-publishing instruction:&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;"devto_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://dev.to/your-name/your-tutorial"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publish"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;This maps to the &lt;code&gt;medium_publish_from_devto&lt;/code&gt; tool. The package fetches the published DEV.to representation, opens Medium's official &lt;a href="https://medium.com/p/import" rel="noopener noreferrer"&gt;&lt;code&gt;/p/import&lt;/code&gt;&lt;/a&gt; route, waits for the editor to save, and returns JSON containing a Medium URL plus details such as title, subtitle, topics, and hero-image detection.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;false&lt;/code&gt; value is the critical control. It asks the pipeline to stop at a draft. Do not treat a successful tool response as proof that the article is ready for live publication. Open the returned editor URL and inspect it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the actual editor
&lt;/h2&gt;

&lt;p&gt;The source Markdown is the ground truth, but the Medium editor is the thing readers will see. Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The title and subtitle match the source.&lt;/li&gt;
&lt;li&gt;Headings are rendered as headings rather than literal Markdown.&lt;/li&gt;
&lt;li&gt;Each code example remains one code block with readable content.&lt;/li&gt;
&lt;li&gt;Lists, links, and paragraphs keep their order.&lt;/li&gt;
&lt;li&gt;Topics describe the article without adding unsupported claims.&lt;/li&gt;
&lt;li&gt;No secret, private URL, or unexpected text crossed the import boundary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package's extraction command reports a structured outline and formatting flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;extract&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://medium.com/p/your-draft/edit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pay special attention to content after the first code block. A malformed import can make later paragraphs look like code, which is easy to miss during a quick visual scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply a targeted correction
&lt;/h2&gt;

&lt;p&gt;If extraction identifies a known issue, create a small actions file and apply only the required fixes:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"removeEmptyCodeBlocks"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mergeAdjacentCodeBlocks"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"promoteDemoteHeading"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"blockIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;Run the correction against the same draft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;medium-publisher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fix-draft&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://medium.com/p/your-draft/edit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--actions-file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fixes.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then extract again and compare the affected blocks with the DEV.to source. Do not repeatedly apply actions when the editor structure has changed. A selector or block index that was correct for one draft can be wrong after an import or a Medium UI change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify success before publishing
&lt;/h2&gt;

&lt;p&gt;A useful verification record contains:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The source DEV.to URL.&lt;/li&gt;
&lt;li&gt;The returned Medium draft URL.&lt;/li&gt;
&lt;li&gt;The extraction result before any fix.&lt;/li&gt;
&lt;li&gt;The actions file, if a fix was needed.&lt;/li&gt;
&lt;li&gt;The extraction result after the fix.&lt;/li&gt;
&lt;li&gt;A human check of title, code, links, metadata, and sensitive data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only after those checks should you decide whether to use a live publication option. The project also provides a CLI path with &lt;code&gt;--publish&lt;/code&gt;, but this tutorial deliberately does not include it in the working command. Keeping the approval step outside the default agent request makes accidental publication less likely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this design works
&lt;/h2&gt;

&lt;p&gt;The workflow separates three kinds of state that are often conflated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source state: the published DEV.to Markdown.&lt;/li&gt;
&lt;li&gt;Review state: the imported Medium draft and its extracted blocks.&lt;/li&gt;
&lt;li&gt;Public state: the final Medium story after an explicit publication action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation makes failures diagnosable. If the import is wrong, you can fix or discard the draft without changing the source. If the session expires, you can log in again without changing the article. If Medium changes its UI, you can stop at review instead of trusting an unattended browser run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and security boundaries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The session is missing or expired
&lt;/h3&gt;

&lt;p&gt;The CLI documents exit code &lt;code&gt;3&lt;/code&gt; for a missing or expired session. Run &lt;code&gt;medium-publisher login&lt;/code&gt; again. Two-factor authentication or a bot challenge may require headed interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  The import does not complete
&lt;/h3&gt;

&lt;p&gt;The DEV.to URL must be public. Check it in a private browser window and confirm that its public API representation contains the article body. An authenticated or unpublished source is not a reliable import input.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium selectors stop matching
&lt;/h3&gt;

&lt;p&gt;Medium's interface can change. The repository specifically targets a &lt;code&gt;contenteditable&lt;/code&gt; import field rather than assuming the first visible HTML input is the article URL field. If the workflow behaves unexpectedly, inspect the draft manually and report the reproducible case instead of publishing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sensitive data appears in the draft
&lt;/h3&gt;

&lt;p&gt;Stop immediately. Do not publish the draft. Remove it through Medium's controls as appropriate, inspect the local browser-state directory, and rotate any credential that may have been exposed. The project's security check can flag secret-like values and unexpected short links, but it cannot replace human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does MCP publish automatically?
&lt;/h3&gt;

&lt;p&gt;MCP only provides the connection between the client and the server. The server's publication capabilities still require authorization. Use &lt;code&gt;publish: false&lt;/code&gt; for the draft-first workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a Medium API key?
&lt;/h3&gt;

&lt;p&gt;No. The documented package uses browser automation and a saved login session because it does not depend on a stable public Medium write API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a personal blog instead of DEV.to?
&lt;/h3&gt;

&lt;p&gt;The package supports importing public URLs generally, but the DEV.to pipeline can compare the imported result with DEV.to source Markdown. That comparison is the most useful part of this tutorial's verification loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the workflow production-proof?
&lt;/h3&gt;

&lt;p&gt;No. Browser UI changes, account challenges, content-specific formatting, and authentication failures remain possible. The project reduces risk with extraction, targeted fixes, security checks, and draft-first behavior; it does not guarantee a successful unattended publication.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize and edit this tutorial. The release, commands, MCP configuration, session paths, limitations, and security boundaries were checked against the public &lt;code&gt;publish-agents&lt;/code&gt; repository at &lt;code&gt;v0.2.3&lt;/code&gt;, its package documentation, and the current MCP specification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;An MCP publishing agent is most useful when it prepares reviewable state instead of silently crossing the publication boundary. Install a pinned release, save the browser session locally, import with &lt;code&gt;publish: false&lt;/code&gt;, compare the Medium editor with the source, and keep live publication explicit.&lt;/p&gt;

&lt;p&gt;What is the most valuable approval checkpoint in your own agent-assisted publishing workflow: authentication, content review, metadata review, or the final publish action?&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>medium</category>
      <category>tutorial</category>
      <category>browserautomation</category>
    </item>
    <item>
      <title>Sync AI Guidance Across Repositories with a Reviewable GitHub Action</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sat, 12 Sep 2026 12:36:29 +0000</pubDate>
      <link>https://dev.to/paladini/sync-ai-guidance-across-repositories-with-a-reviewable-github-action-22p</link>
      <guid>https://dev.to/paladini/sync-ai-guidance-across-repositories-with-a-reviewable-github-action-22p</guid>
      <description>&lt;p&gt;Teams often standardize how they work with AI coding tools by adding files such as &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, Copilot instructions, prompt templates, and editor rules to every repository. The first copy is easy. Keeping those files aligned across dozens of repositories is where the process becomes fragile.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;team-ai-sync&lt;/code&gt; is an open-source GitHub Action that treats shared guidance as source-controlled input. It copies selected files or directories into target repositories, creates a sync branch, and opens or updates pull requests for review. It also exposes a dry-run mode so you can inspect the intended changes before the action pushes anything.&lt;/p&gt;

&lt;p&gt;This tutorial builds the smallest useful GitHub setup, explains the configuration that controls it, and shows where the tool's safety boundary ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep shared AI guidance in one source repository.&lt;/li&gt;
&lt;li&gt;Configure target repositories and paths in &lt;code&gt;sync-config.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;paladini/team-ai-sync@v1&lt;/code&gt; from GitHub Actions.&lt;/li&gt;
&lt;li&gt;Start with &lt;code&gt;dry-run: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Review the generated pull requests before merging them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stable action is documented in the &lt;a href="https://github.com/paladini/team-ai-sync/tree/v1" rel="noopener noreferrer"&gt;&lt;code&gt;v1&lt;/code&gt; repository README&lt;/a&gt;, and the project is licensed under &lt;a href="https://github.com/paladini/team-ai-sync/blob/v1/LICENSE" rel="noopener noreferrer"&gt;MIT&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub source repository containing the shared files.&lt;/li&gt;
&lt;li&gt;One or more target repositories that the token can read and write.&lt;/li&gt;
&lt;li&gt;A fine-grained personal access token or GitHub App installation token.&lt;/li&gt;
&lt;li&gt;Permission to create files, branches, and pull requests in the targets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The action itself runs on Node 24 through its GitHub Action metadata. Your source repository workflow can use the standard &lt;code&gt;ubuntu-latest&lt;/code&gt; runner.&lt;/p&gt;

&lt;p&gt;For GitHub targets, the project documents &lt;code&gt;Contents: Read and write&lt;/code&gt;, &lt;code&gt;Pull requests: Read and write&lt;/code&gt;, and &lt;code&gt;Metadata: Read&lt;/code&gt;. If you use pull request labels, it also documents &lt;code&gt;Issues: Read and write&lt;/code&gt;. Store the token as a repository secret. Do not put it in &lt;code&gt;sync-config.json&lt;/code&gt; or commit it to the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Create the shared files
&lt;/h2&gt;

&lt;p&gt;Use a source repository as the place where the team reviews shared guidance. A small example might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AGENTS.md
CLAUDE.md
.editorconfig
.github/
└── instructions/
    ├── code-review.md
    └── security.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action can sync individual files, whole directories, or both. Keep the source set intentionally narrow. Repository-specific secrets, credentials, local machine settings, and files containing personal data do not belong in a shared guidance bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add &lt;code&gt;sync-config.json&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Create this file in the source repository:&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;"targetRepositories"&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="s2"&gt;"your-org/api-service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"your-org/web-app"&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;"syncMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"overwrite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deleteOrphans"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&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;"AGENTS.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLAUDE.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".editorconfig"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"directories"&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;".github/instructions"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&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;"prOptions"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chore: sync team AI assets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Synced from {{sourceRepo}} at {{sourceCommit}}."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"commitMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chore(ai-assets): sync team assets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chore/team-ai-sync"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"labels"&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;"automation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chore"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"userReviewers"&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;"teamReviewers"&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;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;&lt;code&gt;targetRepositories&lt;/code&gt; is processed one repository at a time. &lt;code&gt;files&lt;/code&gt; names exact repository-relative files. &lt;code&gt;directories&lt;/code&gt; are copied recursively. &lt;code&gt;exclude&lt;/code&gt; can remove specific paths or glob patterns from either input.&lt;/p&gt;

&lt;p&gt;The default &lt;code&gt;syncMode&lt;/code&gt; is &lt;code&gt;overwrite&lt;/code&gt;, which replaces configured target files with the source version. Use &lt;code&gt;skip&lt;/code&gt; when a file should be added only if it does not already exist. &lt;code&gt;deleteOrphans&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt; by default. When enabled, deletion is limited to files inside configured synced directories, but it is still worth testing with a dry run first.&lt;/p&gt;

&lt;p&gt;The placeholders in &lt;code&gt;prOptions.body&lt;/code&gt; are replaced with the source repository and source commit from the workflow context. The generated branch, commit, pull request title, labels, and reviewers are also configured here.&lt;/p&gt;

&lt;p&gt;For every configured source path, the project validates that the path exists and is a file or directory of the expected type before it processes targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Add the GitHub workflow
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/sync-ai-assets.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sync AI Assets&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paladini/team-ai-sync@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TEAM_SYNC_ADMIN_PAT }}&lt;/span&gt;
          &lt;span class="na"&gt;config-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sync-config.json&lt;/span&gt;
          &lt;span class="na"&gt;dry-run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the &lt;code&gt;TEAM_SYNC_ADMIN_PAT&lt;/code&gt; secret in the source repository. The workflow's &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; only needs to read the source checkout. The separate &lt;code&gt;github-token&lt;/code&gt; input is used for target repository operations.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;v1&lt;/code&gt; tag is the stable action reference documented by the project. If your organization requires immutable references, resolve the tag to a commit and apply your own pinning policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Verify the dry run
&lt;/h2&gt;

&lt;p&gt;Push the configuration and workflow, or start the workflow through &lt;code&gt;workflow_dispatch&lt;/code&gt;. With &lt;code&gt;dry-run: true&lt;/code&gt;, the action validates the configuration, clones target repositories into temporary worktrees, copies the selected paths, and reports whether changes would be made. It does not push branches or create pull requests.&lt;/p&gt;

&lt;p&gt;Check the run for three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every target was reachable with the configured token.&lt;/li&gt;
&lt;li&gt;The changed paths are limited to the files and directories you intended.&lt;/li&gt;
&lt;li&gt;The output identifies failures per target instead of hiding them behind one aggregate result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the dry run is correct, remove &lt;code&gt;dry-run: true&lt;/code&gt; or set it to &lt;code&gt;false&lt;/code&gt;. The real workflow creates or resets the configured sync branch, commits only when there are changes, pushes that branch, and creates or updates one pull request per changed target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expected result
&lt;/h2&gt;

&lt;p&gt;After a real run, each changed target should have a review request from the configured sync branch. The target owner can inspect the diff, request changes, and merge it through the repository's normal controls.&lt;/p&gt;

&lt;p&gt;The action does not merge pull requests, approve them, bypass branch protection, or change target repository settings. That is the important operational distinction: synchronization is automated, while acceptance remains a repository-owner decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works better than manual copying
&lt;/h2&gt;

&lt;p&gt;The source repository becomes the review boundary for shared guidance. A change to a common instruction file is made once, tested once, and distributed as an ordinary pull request diff. Each target retains its own history and can reject or delay the update.&lt;/p&gt;

&lt;p&gt;The configuration also makes scope explicit. Instead of granting a workflow permission to rewrite arbitrary paths, it names the files and directories that are eligible for synchronization. The implementation rejects absolute paths, &lt;code&gt;..&lt;/code&gt; traversal, &lt;code&gt;.git&lt;/code&gt; paths, and values that resolve outside the repository root.&lt;/p&gt;

&lt;p&gt;The project supports GitHub Actions, a GitLab CI/CD Component, and a Bitbucket Pipe. The configuration shape is similar, but the supported operating model is platform-specific. A GitHub workflow should target GitHub repositories rather than silently mixing platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and limitations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The token can reach too much
&lt;/h3&gt;

&lt;p&gt;The action can clone targets, push branches, create pull requests, apply labels, and request reviewers. Use a fine-grained token or GitHub App installation with the smallest target set that supports the rollout. A path validator cannot compensate for an overpowered credential.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;deleteOrphans&lt;/code&gt; can remove synced files
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;deleteOrphans&lt;/code&gt; is enabled, files removed from a configured source directory can be removed from the corresponding target directory. Keep it disabled during the pilot, then enable it only with a reviewed dry-run diff.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared guidance is not automatically correct
&lt;/h3&gt;

&lt;p&gt;The action distributes files; it does not decide whether an instruction is appropriate for every repository. Keep target-specific rules out of the shared bundle, and review changes for tool compatibility, scope, and unintended policy conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  It does not merge the result
&lt;/h3&gt;

&lt;p&gt;Generated pull requests still need review and merge. That is deliberate. If your process requires approvals, status checks, or security review, configure those controls in the target repositories.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I sync directories such as &lt;code&gt;.cursor/rules&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Yes. Add the directory to &lt;code&gt;directories&lt;/code&gt; and keep the path repository-relative. The project documents &lt;code&gt;.cursor/rules/**&lt;/code&gt;, &lt;code&gt;.github/instructions/**&lt;/code&gt;, and &lt;code&gt;.github/prompts/**&lt;/code&gt; as common use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I preview changes without creating a pull request?
&lt;/h3&gt;

&lt;p&gt;Yes. Set &lt;code&gt;dry-run: true&lt;/code&gt;. The dry run validates and simulates the sync without pushing branches or opening pull requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it support files outside GitHub?
&lt;/h3&gt;

&lt;p&gt;The project includes packages for GitLab and Bitbucket, but each package is intended for repositories hosted on its own platform. Use the package matching the target platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where should secrets go?
&lt;/h3&gt;

&lt;p&gt;Do not sync secrets, private keys, credentials, or machine-specific configuration. Store the action token in the source repository's secret store and keep the synchronized bundle limited to reviewable guidance and settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize and edit this tutorial. The commands, configuration fields, security boundaries, stable action reference, and verification results were checked against the public &lt;code&gt;paladini/team-ai-sync&lt;/code&gt; repository at the &lt;code&gt;v1&lt;/code&gt; tag and a clean checkout of that tag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;If your team maintains AI guidance in more than one repository, make the source explicit and distribute it through reviewable changes. Start with a small target set, run &lt;code&gt;dry-run: true&lt;/code&gt;, inspect the paths and permissions, and expand only after the pull request workflow is predictable.&lt;/p&gt;

&lt;p&gt;What is the first shared file your team would synchronize: &lt;code&gt;AGENTS.md&lt;/code&gt;, editor rules, prompt templates, or something else?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>githubactions</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Add 36 Local Developer Utilities to Cursor with an MCP Plugin</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Fri, 11 Sep 2026 23:56:21 +0000</pubDate>
      <link>https://dev.to/paladini/add-36-local-developer-utilities-to-cursor-with-an-mcp-plugin-13la</link>
      <guid>https://dev.to/paladini/add-36-local-developer-utilities-to-cursor-with-an-mcp-plugin-13la</guid>
      <description>&lt;p&gt;If you repeatedly leave Cursor to generate a UUID, decode a JWT, calculate a CIDR range, or format JSON, the problem is not the individual command. The problem is the missing tool boundary inside your coding workflow.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/paladini/devutils-cursor-plugin" rel="noopener noreferrer"&gt;DevUtils MCP Plugin&lt;/a&gt; packages a local MCP server for Cursor and Claude Code. It exposes 36 small developer utilities for hashing, encoding, JSON, JWTs, UUIDs, text, and network calculations. The plugin does not require an API key, and its configuration starts the server with &lt;code&gt;npx&lt;/code&gt; on your own machine.&lt;/p&gt;

&lt;p&gt;This tutorial shows the smallest useful path: install the plugin, inspect the configuration, enable the MCP server, and verify the result with a few safe requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install &lt;code&gt;DevUtils MCP&lt;/code&gt; from Cursor's Customize page, enable the &lt;code&gt;devutils&lt;/code&gt; server, then ask your agent to generate a UUID or validate JSON. The plugin wrapper is public and MIT licensed. The underlying &lt;code&gt;devutils-mcp-server&lt;/code&gt; is currently version &lt;code&gt;1.1.0&lt;/code&gt; on npm and requires Node.js 18 or newer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursor with plugin and MCP support.&lt;/li&gt;
&lt;li&gt;Node.js 18 or newer available as &lt;code&gt;node&lt;/code&gt; and &lt;code&gt;npx&lt;/code&gt; in your PATH.&lt;/li&gt;
&lt;li&gt;Permission to download the public npm package the first time the server starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The plugin repository itself is &lt;a href="https://github.com/paladini/devutils-cursor-plugin/blob/main/LICENSE" rel="noopener noreferrer"&gt;public and MIT licensed&lt;/a&gt;. The repository's current plugin manifests declare version &lt;code&gt;1.0.7&lt;/code&gt;. The server is a separate package, so keep those two versions distinct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the plugin in Cursor
&lt;/h2&gt;

&lt;p&gt;Cursor documents plugins as bundles that can include MCP servers, rules, skills, agents, commands, and hooks. Its current installation flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Cursor's &lt;strong&gt;Customize&lt;/strong&gt; page.&lt;/li&gt;
&lt;li&gt;Find the plugin in the marketplace or use the community plugin flow.&lt;/li&gt;
&lt;li&gt;Search for &lt;strong&gt;DevUtils MCP&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Install&lt;/strong&gt; and select the scope you want.&lt;/li&gt;
&lt;li&gt;Enable the &lt;code&gt;devutils&lt;/code&gt; MCP server in the installed plugin.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://cursor.com/docs/plugins" rel="noopener noreferrer"&gt;Cursor plugin documentation&lt;/a&gt; describes the marketplace and installation flow. If the plugin is not listed in your account yet, the repository README documents the direct &lt;strong&gt;Add from GitHub&lt;/strong&gt; route with &lt;code&gt;paladini/devutils-cursor-plugin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After installation, reload Cursor if the MCP server does not appear immediately. Then open the MCP section in Customize and confirm that &lt;code&gt;devutils&lt;/code&gt; is enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the one-file configuration
&lt;/h2&gt;

&lt;p&gt;The repository's current &lt;code&gt;mcp.json&lt;/code&gt; contains this exact server definition:&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;"mcpServers"&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;"devutils"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"devutils-mcp-server"&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;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;There are two useful details here. First, &lt;code&gt;npx -y&lt;/code&gt; can install the named package when it is not already available locally. Second, the server is a stdio process. Cursor starts it and communicates with it through the MCP protocol; you do not need to expose an HTTP port or copy an API key into a configuration file.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/paladini/devutils-mcp-server" rel="noopener noreferrer"&gt;MCP server repository&lt;/a&gt; is the primary source for the tool catalog and server implementation. The plugin is a distribution wrapper that makes the configuration discoverable from Cursor and Claude Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the minimal workflow
&lt;/h2&gt;

&lt;p&gt;Once the server is enabled, ask your agent for a small operation whose input is not sensitive. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate a UUID v4 with the DevUtils MCP server.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then try a deterministic transformation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pretty-print this JSON and validate it with DevUtils:
{"project":"demo","enabled":true}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also ask for a calculation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calculate the network details for 10.0.0.0/24 with DevUtils.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected result is an agent response backed by a named DevUtils tool. The exact UUID value changes on every generation. For the JSON request, you should see formatted JSON and a valid result. For the CIDR request, expect the network address, broadcast address, host range, and host count.&lt;/p&gt;

&lt;p&gt;The server also exposes tools for SHA hashing, Base64 and hexadecimal conversion, JWT decoding, date and timestamp conversion, regex checks, text statistics, URL encoding, and other small operations. The &lt;a href="https://github.com/paladini/devutils-mcp-server#available-tools" rel="noopener noreferrer"&gt;available-tools section in the server README&lt;/a&gt; is the authoritative catalog, so check it before writing prompts that depend on a particular name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the process without Cursor
&lt;/h2&gt;

&lt;p&gt;The plugin is optional. Any MCP client that accepts a stdio server can use the same package directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; devutils-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command is a long-running server process, not a traditional command that prints a result and exits. A successful startup keeps the process attached to stdio. Stop it with &lt;code&gt;Ctrl+C&lt;/code&gt; when testing manually.&lt;/p&gt;

&lt;p&gt;In my validation of the current package, &lt;code&gt;npm view devutils-mcp-server version license engines&lt;/code&gt; returned version &lt;code&gt;1.1.0&lt;/code&gt;, MIT licensing, and the Node.js engine requirement &lt;code&gt;&amp;gt;=18&lt;/code&gt;. Starting the package with &lt;code&gt;npx -y devutils-mcp-server --help&lt;/code&gt; reached the server and reported &lt;code&gt;DevUtils MCP Server running on stdio&lt;/code&gt;; because it is a server, the process needed to be stopped after startup. That verifies launch, not every individual tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this setup works
&lt;/h2&gt;

&lt;p&gt;The plugin separates distribution from execution. The repository contributes manifests, the MCP configuration, documentation, and privacy and security policies. The npm package contributes the executable server and tool handlers. Cursor only needs to understand the MCP entry in &lt;code&gt;mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That separation also keeps the example portable. Claude Code can install the plugin with its marketplace command, while another MCP client can run the npm package directly. The configuration remains a small, reviewable JSON file rather than a collection of copied shell commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and boundaries
&lt;/h2&gt;

&lt;p&gt;If the server does not appear in Cursor, check the plugin scope, reload Cursor, and confirm that &lt;code&gt;npx&lt;/code&gt; resolves in the environment used by the editor. If startup fails, run &lt;code&gt;node --version&lt;/code&gt; and &lt;code&gt;npx --version&lt;/code&gt; in a terminal, then check whether npm can reach the registry.&lt;/p&gt;

&lt;p&gt;If a request fails, verify the tool exists in the current server README. The plugin repository says 36 utilities, but the server package is independently versioned and can change. Do not assume a tool name from an old prompt or screenshot.&lt;/p&gt;

&lt;p&gt;Local execution is not the same as a security guarantee. Inputs are processed by the local server according to the implementation, but &lt;code&gt;npx&lt;/code&gt; still downloads a package from npm on first use. Review the package, lock down your normal npm trust controls, and avoid sending production secrets, private tokens, or customer data to any tool until you have reviewed its source and behavior. JWT decoding is not JWT verification, and hashing does not make a sensitive value safe to disclose.&lt;/p&gt;

&lt;p&gt;The plugin does not provide a cloud API, authentication layer, or remote dashboard. It also does not replace a secrets manager, a security scanner, or a full network diagnostic system. Its value is narrow: convenient, repeatable local utilities inside an agent workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does it need an API key?
&lt;/h3&gt;

&lt;p&gt;No API key is required by the plugin or the documented server configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it work outside Cursor?
&lt;/h3&gt;

&lt;p&gt;Yes. The plugin README documents direct server usage for other MCP clients, including Claude Code, VS Code, Windsurf, and Docker references.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does local mean no network activity?
&lt;/h3&gt;

&lt;p&gt;No. The tool operations run in the local server process, but &lt;code&gt;npx&lt;/code&gt; may download &lt;code&gt;devutils-mcp-server&lt;/code&gt; from npm when it is first run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can it verify a JWT signature?
&lt;/h3&gt;

&lt;p&gt;The documented JWT utility decodes and validates token structure and expiration-related information. Treat decoding as inspection, not cryptographic signature verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A useful MCP integration does not need to be a large platform. A public manifest, a one-file stdio configuration, and small deterministic tools are enough to remove repetitive context switching from an agent workflow. Install DevUtils when those utilities belong inside your local development loop, and keep the server's version, source, and input boundaries visible to the team.&lt;/p&gt;

&lt;p&gt;What small local utility would save you the most context switching inside your coding agent: JSON validation, token inspection, hashing, or something else?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI assistance disclosure:&lt;/strong&gt; This tutorial was researched and drafted with AI assistance. Repository files, package metadata, documented commands, and the startup behavior described here were checked against the current public sources before publication.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>mcp</category>
      <category>cursor</category>
      <category>tutorial</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Download Authorized Circle Course Videos with Python and Playwright</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Thu, 10 Sep 2026 12:39:40 +0000</pubDate>
      <link>https://dev.to/paladini/download-authorized-circle-course-videos-with-python-and-playwright-9be</link>
      <guid>https://dev.to/paladini/download-authorized-circle-course-videos-with-python-and-playwright-9be</guid>
      <description>&lt;p&gt;If you have permission to watch a Circle course but your connection is unreliable, saving it for later is harder than it sounds. The useful data is behind a browser session, course pages can contain many lessons, and a downloader should not turn authentication into a pile of copied cookies.&lt;/p&gt;

&lt;p&gt;This tutorial shows a local workflow with &lt;a href="https://github.com/paladini/circle-course-downloader" rel="noopener noreferrer"&gt;Circle Course Downloader&lt;/a&gt;. It opens a dedicated Chromium profile for you to sign in normally, discovers video URLs from pages you can already access, and delegates the media transfer to &lt;code&gt;yt-dlp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important boundary is authorization: use this only for content you are allowed to view and download. The project is unofficial and does not bypass DRM, paywalls, two-factor authentication, captchas, or access controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install version 0.1.4 from PyPI, install the Chromium runtime, and run a dry run against a course URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;playwright&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-community.example.com/c/course-slug"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, a visible Chromium window lets you authenticate. The tool stores the session in its own local profile, discovers lessons, and prints the &lt;code&gt;yt-dlp&lt;/code&gt; commands without downloading media. Remove &lt;code&gt;--dry-run&lt;/code&gt; only after checking the result and confirming your rights to save the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need Python 3.11 or newer, an account authorized for the Circle community and specific content, Chromium installed through Playwright, and enough local disk space for the media.&lt;/p&gt;

&lt;p&gt;The package is MIT licensed and currently early-stage software. Version 0.1.4 is available on &lt;a href="https://pypi.org/project/circle-course-downloader/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; and the source is maintained in the &lt;a href="https://github.com/paladini/circle-course-downloader" rel="noopener noreferrer"&gt;public GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create a virtual environment when you want the CLI isolated from other Python tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;venv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;venv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\.venv\Scripts\Activate.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;playwright&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start with a dry run
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;download&lt;/code&gt; for a full course page. Replace the example hostname and slug with a URL from your own community:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-community.example.com/c/course-slug"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run opens a dedicated Chromium profile. Sign in in that window, return to the terminal, and press &lt;code&gt;Enter&lt;/code&gt;. This is deliberately browser-first. You do not paste a password into the terminal, export cookies from your default browser, or place a token in a command argument.&lt;/p&gt;

&lt;p&gt;After authentication, the downloader visits the course page, finds lesson links, inspects each lesson for supported video providers, and prints the commands it would use. The supported list includes Circle HLS, direct video URLs, YouTube, Vimeo, Wistia, Loom, Mux, and Cloudflare Stream.&lt;/p&gt;

&lt;p&gt;The dry run is a decision point. Review the discovered lesson names and URLs. If the page includes material you are not authorized to download, stop there or narrow the operation according to the project capabilities and service rules.&lt;/p&gt;

&lt;p&gt;For a single community post or event page with one embedded video, use the separate command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download-standalone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-community.example.com/c/space-slug/post-slug"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--dry-run&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commands are not interchangeable. &lt;code&gt;download&lt;/code&gt; expects a course structure with sections and lessons. &lt;code&gt;download-standalone&lt;/code&gt; targets one page with one embedded video.&lt;/p&gt;

&lt;h2&gt;
  
  
  Download after checking the plan
&lt;/h2&gt;

&lt;p&gt;When the dry-run output matches your intended scope, run the same command without &lt;code&gt;--dry-run&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-community.example.com/c/course-slug"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, media and manifests go under &lt;code&gt;downloads/&lt;/code&gt;. Use &lt;code&gt;--output-dir&lt;/code&gt; for a dedicated destination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-community.example.com/c/course-slug"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output-dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./authorized-course-export"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The downloader uses &lt;code&gt;yt-dlp&lt;/code&gt; with resume support and MP4 merge output. This helps with long courses and interrupted connections, but it does not guarantee that every provider or stream will work forever. Provider changes, expired signed URLs, unavailable lessons, and codec issues can still affect individual downloads.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/yt-dlp/yt-dlp#output-template-and-path" rel="noopener noreferrer"&gt;yt-dlp documentation&lt;/a&gt; describes its output and continuation behavior. That downstream tool receives URLs discovered from the authorized browser session, so protect those URLs like credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the local files
&lt;/h2&gt;

&lt;p&gt;The default layout is easy to inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.auth/session-browser-profile/
.auth/session.json
downloads/manifest.json
downloads/manifest.csv
downloads/01 - Lesson title.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifest records what discovery found. Treat it as an audit artifact, not disposable noise. It can contain signed media URLs, so do not publish or commit it.&lt;/p&gt;

&lt;p&gt;The session files are even more sensitive. Playwright's &lt;a href="https://playwright.dev/python/docs/auth" rel="noopener noreferrer"&gt;authentication documentation&lt;/a&gt; explains that saved browser state can contain cookies, local storage, and other credentials that may impersonate an account. Keep &lt;code&gt;.auth/&lt;/code&gt; local and add it to &lt;code&gt;.gitignore&lt;/code&gt; in any workspace where you use the tool.&lt;/p&gt;

&lt;p&gt;This design follows Playwright's storage-state pattern: authenticate once in a controlled browser context and reuse that state later. It is safer than copying cookies from a personal browser, but it is not a security boundary. Anyone who obtains the stored state may be able to act as you until it expires or is revoked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful options
&lt;/h2&gt;

&lt;p&gt;Both commands accept the same operational options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-community.example.com/c/course-slug"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output-dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./downloads"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--session&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./.auth/session.json"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--headless&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;--session&lt;/code&gt; to choose a different exported session location. The browser profile is stored next to it. Use &lt;code&gt;--force-login&lt;/code&gt; when the existing session is stale and you want a fresh visible login. Use &lt;code&gt;--headless&lt;/code&gt; only after a saved browser profile exists. A first login requires a visible browser window.&lt;/p&gt;

&lt;p&gt;The CLI exposes the exact options installed on your machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download-standalone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verify the installation locally
&lt;/h2&gt;

&lt;p&gt;You can verify the installed CLI without logging into a community:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;circle-course-downloader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;download-standalone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--help&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a source checkout, the repository's development checks include tests, Python compilation, wheel building, and package metadata validation. At the current &lt;code&gt;v0.1.4&lt;/code&gt; release commit, those checks completed successfully in a clean checkout: 17 tests passed, &lt;code&gt;compileall&lt;/code&gt; passed, the package build produced both sdist and wheel, and &lt;code&gt;twine check&lt;/code&gt; passed for both artifacts.&lt;/p&gt;

&lt;p&gt;Those results validate the package and parser logic. They do not prove that a particular Circle community, lesson, provider, or account session will work today. The meaningful end-to-end check for your account is a small authorized dry run followed by one or two downloads that you can inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and limits
&lt;/h2&gt;

&lt;p&gt;If Chromium does not start, install the browser runtime with &lt;code&gt;python -m playwright install chromium&lt;/code&gt; and check that the active Python environment is the one running the CLI.&lt;/p&gt;

&lt;p&gt;If the browser opens but discovery returns no lessons, confirm that the URL is a full course page and that the signed-in account can see the lessons in the browser. For one standalone post, use &lt;code&gt;download-standalone&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;If a session stops working, use &lt;code&gt;--force-login&lt;/code&gt;. Do not work around the problem by copying cookies or tokens from another browser. If a provider URL has expired, rerun discovery instead of sharing the old signed URL.&lt;/p&gt;

&lt;p&gt;The tool does not defeat access controls. It cannot make unavailable content available, and a successful browser login is not proof that downloading is contractually or legally permitted. Check the course license, community terms, and content owner's instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this upload my course to an API?
&lt;/h3&gt;

&lt;p&gt;The project runs the browser and downloader locally and does not require a hosted AI service. It still connects to the community and media providers required to authenticate, discover pages, and transfer authorized files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I reuse my normal Chrome profile?
&lt;/h3&gt;

&lt;p&gt;The documented workflow uses a dedicated Chromium profile. Keep it separate. This avoids exposing unrelated browser cookies and makes the session files easier to identify and protect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use it for any video URL?
&lt;/h3&gt;

&lt;p&gt;No. It is designed around Circle course and community pages and the providers its extractors recognize. It is not a general-purpose access-control bypass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why run &lt;code&gt;--dry-run&lt;/code&gt; first?
&lt;/h3&gt;

&lt;p&gt;It makes the scope visible before media is written. You can inspect discovery results, catch a wrong URL, and review the generated commands before accepting the external content transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The useful pattern is a controlled workflow: authenticate in a dedicated browser, discover only what the account can already access, inspect the plan, download locally, and protect the session and manifest files.&lt;/p&gt;

&lt;p&gt;Circle Course Downloader packages that workflow behind two small commands. Start with &lt;code&gt;--dry-run&lt;/code&gt;, test a narrow authorized scope, and treat the current &lt;code&gt;0.1.4&lt;/code&gt; release as early-stage software whose provider support can change.&lt;/p&gt;

&lt;p&gt;Have you found a reliable way to audit and expire local browser sessions for authorized offline course backups?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to organize and edit this tutorial. The commands, package metadata, repository behavior, test results, security boundaries, and limitations were checked against the public project sources and local validation runs described above.&lt;/p&gt;

</description>
      <category>python</category>
      <category>playwright</category>
      <category>tutorial</category>
      <category>videodownloading</category>
    </item>
    <item>
      <title>Govern AI-Assisted Specs in Git with SpecGov</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Wed, 09 Sep 2026 12:37:13 +0000</pubDate>
      <link>https://dev.to/paladini/govern-ai-assisted-specs-in-git-with-specgov-1ddc</link>
      <guid>https://dev.to/paladini/govern-ai-assisted-specs-in-git-with-specgov-1ddc</guid>
      <description>&lt;p&gt;AI coding agents can produce code faster than a team can review the documents that are supposed to govern that code. A repository may contain requirements, designs, tasks, and verification notes, while Git itself does not tell you whether a change still has a complete chain back to those artifacts.&lt;/p&gt;

&lt;p&gt;SpecGov is an open-source TypeScript CLI and GitHub Action for that specific gap. It discovers common spec-driven workflows, normalizes their files into an artifact graph, and reports missing relationships or code changes that bypass explicit mappings. The useful part is its boundary: it checks repository structure and declared relationships, not whether an AI claim is true.&lt;/p&gt;

&lt;p&gt;In this tutorial, you will build SpecGov from its public repository, run a local check, inspect the graph, add a domain mapping, and wire the result into a pull request workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;SpecGov gives a repository a deterministic governance check for durable development artifacts. It supports GitHub Spec Kit, OpenSpec, Kiro, and configurable Generic/TLC layouts. It runs locally without an account, API key, upload, or network call after installation.&lt;/p&gt;

&lt;p&gt;The current repository documents version &lt;code&gt;1.0.0-rc.2&lt;/code&gt; on the &lt;code&gt;main&lt;/code&gt; branch. At the time of writing, that release candidate was not available from npm, so the commands below use a checkout and local build. Treat this as a current-source tutorial, not proof of a stable package release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;Node.js 20 or newer&lt;/li&gt;
&lt;li&gt;A repository containing specifications, plans, tasks, or other durable development artifacts&lt;/li&gt;
&lt;li&gt;A shell with permission to install dependencies and run Node.js&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is released under the MIT license. Its README and package metadata identify Fernando Paladini as the author and &lt;code&gt;paladini/specgov&lt;/code&gt; as the source repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build and run the minimal path
&lt;/h2&gt;

&lt;p&gt;Clone the repository and build the CLI:&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/paladini/specgov.git
&lt;span class="nb"&gt;cd &lt;/span&gt;specgov
npm ci
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the generated CLI against the checkout itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node dist/cli.js check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository currently reports an advisory pass with the Generic framework, two change sets, seven artifacts, and zero findings. Your output will differ if you run it against another repository or a newer commit. The important result is the shape of the report: detected frameworks, change sets, artifacts, findings, evidence, and remediation details.&lt;/p&gt;

&lt;p&gt;You can inspect the normalized graph as Markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node dist/cli.js graph &lt;span class="nt"&gt;--format&lt;/span&gt; markdown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The graph lists artifact IDs, paths, roles, and relationships such as &lt;code&gt;derives_from&lt;/code&gt; and &lt;code&gt;verifies&lt;/code&gt;. This is more useful than a directory listing because it makes the relationships that governance depends on visible and reviewable.&lt;/p&gt;

&lt;p&gt;Finally, preview the configuration SpecGov would create from detected workflows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node dist/cli.js init &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command prints a &lt;code&gt;specgov/v1&lt;/code&gt; manifest without changing the repository. That makes it a safe first step when you are inheriting an existing spec layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an explicit domain mapping
&lt;/h2&gt;

&lt;p&gt;Autodetection is useful for common layouts, but path-sensitive governance should be explicit. Add a &lt;code&gt;.specgov.yml&lt;/code&gt; file when a code area must be connected to particular artifacts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;specgov/v1&lt;/span&gt;
&lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;advisory&lt;/span&gt;
&lt;span class="na"&gt;frameworks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;

&lt;span class="na"&gt;domains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;authentication&lt;/span&gt;
    &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/auth/**"&lt;/span&gt;
    &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;specs/auth/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.kiro/specs/auth/**"&lt;/span&gt;

&lt;span class="na"&gt;policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;require_complete_chain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;require_change_artifact_for_code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;require_verification_evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;stale_after_days&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;domains&lt;/code&gt; block says which artifacts govern which code. The policy flags then define the minimum structural chain. A changed specification in an unrelated directory does not satisfy an explicit domain, which prevents a generic document from hiding a missing authentication-specific change.&lt;/p&gt;

&lt;p&gt;Run the check again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node dist/cli.js check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start with &lt;code&gt;mode: advisory&lt;/code&gt; while you learn the repository's actual shape. Once the findings are understood, strict mode can make policy findings fail the command. The project documents exit code &lt;code&gt;1&lt;/code&gt; for strict policy failures and exit code &lt;code&gt;2&lt;/code&gt; for configuration or runtime errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce the result in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;SpecGov also ships a GitHub Action. The documented workflow checks out the full history and compares the pull request base and head revisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v5&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paladini/specgov@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;strict&lt;/span&gt;
    &lt;span class="na"&gt;base-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.base.sha }}&lt;/span&gt;
    &lt;span class="na"&gt;head-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Action writes a Markdown summary and exposes status, report JSON, graph JSON, detected frameworks, and finding count. Full history matters because the comparison needs both revisions. The &lt;code&gt;v1&lt;/code&gt; Action reference is separate from the current npm-style package version shown in &lt;code&gt;package.json&lt;/code&gt;, so check the project's release guidance before pinning a production workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;SpecGov separates three questions that are often mixed together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which artifact workflows exist in this repository?&lt;/li&gt;
&lt;li&gt;Are the declared files and relationships structurally complete?&lt;/li&gt;
&lt;li&gt;Is the content semantically correct and truthful?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first two are deterministic enough for a local command and a pull request check. SpecGov can prove that files, states, relationships, and explicit path mappings satisfy configured structural policy. It cannot prove that a requirement is well designed, that a test really covers a behavior, or that a declared AI producer actually created a file. Optional semantic auditors can add review signals, but they do not change that boundary.&lt;/p&gt;

&lt;p&gt;That limitation is a feature for CI. A governance check should report evidence that a machine can reproduce, not turn an unverified declaration into a security guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The documented &lt;code&gt;npx&lt;/code&gt; command cannot resolve
&lt;/h3&gt;

&lt;p&gt;The README shows &lt;code&gt;npx specgov check&lt;/code&gt;, but the checked npm registry query did not find &lt;code&gt;specgov@1.0.0-rc.2&lt;/code&gt;. Use the checkout workflow above until the package is published, or confirm the exact version in the registry before sharing an &lt;code&gt;npx&lt;/code&gt; command.&lt;/p&gt;

&lt;h3&gt;
  
  
  The check passes but the change is still wrong
&lt;/h3&gt;

&lt;p&gt;That is expected. A pass means the structural policy was satisfied. It does not validate business logic, semantic quality, authorship, or the correctness of an external claim. Keep normal code review, tests, and security review in the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  A strict check fails unexpectedly
&lt;/h3&gt;

&lt;p&gt;Inspect the report and graph first. Look for an incomplete artifact chain, a missing domain mapping, a stale artifact, or a base/head reference that is not available in the checkout. Run advisory mode locally to understand the findings, then make the smallest explicit mapping that represents the intended workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Old manifests stop working
&lt;/h3&gt;

&lt;p&gt;SpecGov v1 is an intentional break from the v0.1 contract. The upgrade guide says that old &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;artifacts&lt;/code&gt;, &lt;code&gt;mappings&lt;/code&gt;, and &lt;code&gt;rules&lt;/code&gt; fields and the old &lt;code&gt;scan&lt;/code&gt;, &lt;code&gt;check-pr&lt;/code&gt;, &lt;code&gt;trace&lt;/code&gt;, and &lt;code&gt;drift&lt;/code&gt; commands are unsupported. Use &lt;code&gt;specgov init --dry-run&lt;/code&gt;, convert trusted mappings into &lt;code&gt;domains&lt;/code&gt;, and replace old PR checks with &lt;code&gt;specgov check --base-ref ... --head-ref ...&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does SpecGov upload repository content?
&lt;/h3&gt;

&lt;p&gt;The project documents local and deterministic behavior by default, with no account, API key, upload, or network call. Verify the version and configuration you deploy, and review the repository's security policy for your environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it replace a spec-authoring framework?
&lt;/h3&gt;

&lt;p&gt;No. It governs artifacts created by workflows such as GitHub Spec Kit, OpenSpec, Kiro, or a Generic/TLC layout. It is a governance layer, not another authoring system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use it without GitHub Actions?
&lt;/h3&gt;

&lt;p&gt;Yes. The CLI is the primary local path. The Action is an optional enforcement boundary for pull requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The practical value of SpecGov is not that it makes AI-assisted development automatically correct. It makes the repository's declared development chain inspectable: code, artifacts, relationships, and policy become evidence that a local command and CI can reproduce.&lt;/p&gt;

&lt;p&gt;If you try this on a real repository, begin with &lt;code&gt;init --dry-run&lt;/code&gt;, review the graph, and keep advisory mode until the findings match your team's intended workflow. Then decide which structural rules are strong enough to block a pull request.&lt;/p&gt;

&lt;p&gt;This tutorial was prepared with AI assistance. The repository documentation, package metadata, current configuration example, checkout build, tests, lint, typecheck, format check, audit, and CLI smoke commands were checked against the public project state at publication time.&lt;/p&gt;

&lt;p&gt;What artifact relationship would you want a pull request check to prove before code could merge?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>Back Up GitHub Repositories to GitLab with Python</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Tue, 08 Sep 2026 02:16:29 +0000</pubDate>
      <link>https://dev.to/paladini/back-up-github-repositories-to-gitlab-with-python-4dpi</link>
      <guid>https://dev.to/paladini/back-up-github-repositories-to-gitlab-with-python-4dpi</guid>
      <description>&lt;p&gt;Keeping every repository on one hosting platform creates a single point of failure for your source code. A second copy helps, but a backup script that treats every repository as public can create a much worse problem: accidental exposure.&lt;/p&gt;

&lt;p&gt;This tutorial walks through &lt;code&gt;backup-github-to-gitlab&lt;/code&gt;, an open-source Python tool that lists a GitHub user's repositories, creates matching GitLab projects, and mirrors branches and tags over SSH. The workflow defaults private when it cannot determine visibility, supports a dry run, and can be run again to push incremental updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The smallest useful workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Python dependencies and Git.&lt;/li&gt;
&lt;li&gt;Create a YAML configuration and put API tokens in environment variables.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python backup.py --dry-run&lt;/code&gt; to inspect the plan.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python backup.py&lt;/code&gt; after checking the proposed visibility.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tool is a repository backup, not a complete disaster-recovery system. It does not transfer Git LFS objects, organization repositories, or wikis in its current version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need Python 3.11 or newer, Git on your &lt;code&gt;PATH&lt;/code&gt;, and SSH keys registered with both GitHub and GitLab. The project also expects a GitHub token with &lt;code&gt;repo&lt;/code&gt; scope and a GitLab token with &lt;code&gt;api&lt;/code&gt; scope. Those scopes are used by the API clients to list repositories and create GitLab projects.&lt;/p&gt;

&lt;p&gt;The repository is MIT-licensed and the current source is available in &lt;a href="https://github.com/paladini/backup-github-to-gitlab" rel="noopener noreferrer"&gt;backup-github-to-gitlab on GitHub&lt;/a&gt;. The dependency list includes PyGithub, python-gitlab, python-dotenv, Rich, and PyYAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the tool
&lt;/h2&gt;

&lt;p&gt;Clone the project and install its dependencies:&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 git@github.com:paladini/backup-github-to-gitlab.git
&lt;span class="nb"&gt;cd &lt;/span&gt;backup-github-to-gitlab
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script exposes a normal command-line help screen. &lt;code&gt;python backup.py --help&lt;/code&gt; shows options for dry runs, repository glob filters, verbose Git output, forks, archived repositories, and an alternate configuration path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure credentials and usernames
&lt;/h2&gt;

&lt;p&gt;Copy the checked-in configuration example to &lt;code&gt;config.yaml&lt;/code&gt; and set the GitHub and GitLab usernames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-github-username&lt;/span&gt;

&lt;span class="na"&gt;gitlab&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-gitlab-username&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://gitlab.com&lt;/span&gt;

&lt;span class="na"&gt;backup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;include_forks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;include_archived&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;temp_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./tmp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put the tokens in your process environment or in a local &lt;code&gt;.env&lt;/code&gt; file that is not committed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GITHUB_TOKEN=your-github-token
GITLAB_TOKEN=your-gitlab-token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application loads &lt;code&gt;.env&lt;/code&gt; at runtime. The tokens are not read from &lt;code&gt;config.yaml&lt;/code&gt;, and the README states that they are not written to logs. Treat any token copied into a shell history, terminal recording, or issue as compromised and rotate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preview the backup safely
&lt;/h2&gt;

&lt;p&gt;Before making a GitLab project or pushing objects, run the dry-run mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python backup.py &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner still queries GitHub and checks whether each matching GitLab project exists, but it returns a &lt;code&gt;dry_run&lt;/code&gt; result instead of creating projects, cloning repositories, or pushing Git data. A typical report distinguishes an existing project from a project that would be created, and shows whether the project would be public or private.&lt;/p&gt;

&lt;p&gt;For a narrower preview, use a glob filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python backup.py &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s2"&gt;"client-*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for a first run against a small set of repositories. The default configuration skips forks and includes archived repositories. The command-line flags can override those choices when you intentionally want a different scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the mirror
&lt;/h2&gt;

&lt;p&gt;After reviewing the dry-run output, execute the backup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python backup.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each repository, the runner follows this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask GitHub for repositories owned by the configured user.&lt;/li&gt;
&lt;li&gt;Check whether the matching GitLab project exists.&lt;/li&gt;
&lt;li&gt;Create the GitLab project with the visibility reported by GitHub, or reuse its SSH URL.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git clone --mirror&lt;/code&gt; from the GitHub SSH URL.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git push --mirror&lt;/code&gt; from the temporary mirror to GitLab.&lt;/li&gt;
&lt;li&gt;Remove the temporary clone, including read-only Git files on Windows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The mirror flags matter. A normal working-tree clone is optimized for editing one branch. A mirror clone contains all refs, and a mirror push updates the destination refs to match the source. That makes the operation appropriate for a source backup, while also meaning that the destination should be dedicated to this mirror. Manual changes made directly in GitLab can be overwritten by a later run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reruns are safe enough for routine backups
&lt;/h2&gt;

&lt;p&gt;The tool checks for an existing project before creating one. On a later run it gets that project's SSH URL, creates a fresh temporary mirror, and pushes the current GitHub refs. This makes the process idempotent at the project level: rerunning it does not create duplicate GitLab projects.&lt;/p&gt;

&lt;p&gt;It is not a versioned backup in the archival sense. If a branch or tag is removed from GitHub, &lt;code&gt;git push --mirror&lt;/code&gt; can remove the corresponding ref in GitLab. If you need point-in-time recovery, add a separate retention policy such as dated bare-mirror snapshots or storage-level backups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Visibility and security boundaries
&lt;/h2&gt;

&lt;p&gt;Visibility is the most important safety property in this workflow. The GitHub client maps the API's &lt;code&gt;private&lt;/code&gt; value into a repository record. The GitLab client creates a project as &lt;code&gt;private&lt;/code&gt; when that value is true and as &lt;code&gt;public&lt;/code&gt; otherwise. The README also documents a private-by-default policy when visibility cannot be determined, so verify the actual project settings before pushing sensitive code.&lt;/p&gt;

&lt;p&gt;The tool uses SSH for Git transport and API tokens for metadata operations. That separates repository content transfer from API authentication, but it does not remove the need to protect either credential. Use a dedicated GitLab account or group when appropriate, review token scopes, and test the process with a non-sensitive repository first.&lt;/p&gt;

&lt;p&gt;The dry run is a planning aid, not a permission boundary. It can show what the program intends to do, but it cannot prove that a token has the right permissions or that an SSH key can push. A small real run is the discriminating test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes to expect
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The configuration file is missing
&lt;/h3&gt;

&lt;p&gt;The default path is &lt;code&gt;config.yaml&lt;/code&gt;. Create it from &lt;code&gt;config.example.yaml&lt;/code&gt;, or pass another file with &lt;code&gt;--config&lt;/code&gt;. The validator reports missing usernames and missing &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; or &lt;code&gt;GITLAB_TOKEN&lt;/code&gt; before the runner starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git authentication fails
&lt;/h3&gt;

&lt;p&gt;The API tokens do not authenticate the Git SSH connection. Confirm that &lt;code&gt;ssh -T git@github.com&lt;/code&gt; and the equivalent GitLab SSH check work for the account that owns the destination projects. With &lt;code&gt;--verbose&lt;/code&gt;, the tool exposes Git command output that can help isolate an SSH problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  A GitLab project already contains unrelated data
&lt;/h3&gt;

&lt;p&gt;The project intentionally pushes a full mirror. Do not point it at a destination that people edit independently. Create a dedicated project or confirm that replacing its refs is acceptable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate limits or transient API errors occur
&lt;/h3&gt;

&lt;p&gt;The GitHub client waits for a core API rate-limit reset. GitLab HTTP 429 responses are retried up to four times with a 60-second delay. A failure after those retries is reported for that repository, while the runner continues processing the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;The current README explicitly lists three limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git LFS objects are not transferred by &lt;code&gt;git clone --mirror&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Organization repositories are not included in version 1.&lt;/li&gt;
&lt;li&gt;GitLab pull mirroring for private repositories requires GitLab Premium, and the planned GitHub Actions alternative is not implemented here.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wikis are also listed as a future roadmap item. If any of these are part of your recovery objective, treat this tool as one layer of the backup plan and verify those assets separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does the tool copy private repositories as public?
&lt;/h3&gt;

&lt;p&gt;The implementation maps GitHub visibility to the GitLab project. Private repositories are created as private. Still inspect the dry-run output and destination settings before using it for sensitive repositories.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I back up only one repository?
&lt;/h3&gt;

&lt;p&gt;Yes. Use a matching glob, such as &lt;code&gt;--filter "my-project"&lt;/code&gt;, and run a dry run first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it need a GitHub Actions workflow?
&lt;/h3&gt;

&lt;p&gt;No. It runs locally and uses the GitHub and GitLab APIs plus SSH Git operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this a replacement for snapshots?
&lt;/h3&gt;

&lt;p&gt;No. It keeps a GitLab mirror current. Add retention and independent storage if you need historical recovery after deletions or corruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;backup-github-to-gitlab&lt;/code&gt; is a focused way to create a second Git hosting copy without writing custom API and Git orchestration code. Its strongest design choices are the dry-run mode, visibility-aware project creation, mirror refs, and cleanup of temporary clones. Start with one harmless repository, verify both API and SSH authentication, inspect the proposed visibility, and only then expand the filter.&lt;/p&gt;

&lt;p&gt;What would you add first for your own backup policy: Git LFS support, organization repositories, or retention-aware snapshots?&lt;/p&gt;

&lt;p&gt;Disclosure: AI assistance was used to organize this tutorial and review its wording. The commands, behavior, limitations, and project details were checked against the repository's current public README, source files, configuration example, dependency list, license, and public GitHub metadata.&lt;/p&gt;

</description>
      <category>python</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>gitlab</category>
    </item>
    <item>
      <title>Harness Score 1.6.4: Safer Scans, Broader CI, and Trustworthy Docs</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sat, 05 Sep 2026 17:14:57 +0000</pubDate>
      <link>https://dev.to/paladini/harness-score-164-safer-scans-broader-ci-and-trustworthy-docs-f1a</link>
      <guid>https://dev.to/paladini/harness-score-164-safer-scans-broader-ci-and-trustworthy-docs-f1a</guid>
      <description>&lt;p&gt;AI coding agents do not work in a vacuum. Their results depend on the context files, rules, skills, hooks, tests, CI pipelines, and safety controls that surround the model. That surrounding system is the harness: the part of an agentic workflow that a team can actually design, version, test, and improve.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/paladini/harness-score" rel="noopener noreferrer"&gt;Harness Score&lt;/a&gt; turns that idea into a deterministic repository audit. It checks 36 concrete signals across six dimensions, assigns a maturity level from L0 to L4, and shows the next gaps to close. It runs with zero LLM calls, zero network access during a scan, and zero runtime dependencies.&lt;/p&gt;

&lt;p&gt;Since version 1.5.3, the project has gone through a focused release train. The goal was not to inflate the score or add flashy checks. It was to make the result safer to trust across large repositories, monorepos, self-hosted CI systems, and five documentation languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Harness Score can now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;audit the structural readiness of an AI coding harness across Cursor, Claude Code, Codex, Windsurf, Cline, Continue, OpenCode, Zed, and other supported conventions;&lt;/li&gt;
&lt;li&gt;fail closed instead of presenting a partial filesystem walk as an authoritative score;&lt;/li&gt;
&lt;li&gt;inspect complete, deeply nested repositories with a much larger safety fuse;&lt;/li&gt;
&lt;li&gt;recognize CI configuration inside nested workspaces, Google Cloud Build directories, and Forgejo Actions;&lt;/li&gt;
&lt;li&gt;produce terminal, JSON, Markdown, badge, diff, and GitHub Action outputs;&lt;/li&gt;
&lt;li&gt;keep its check catalog, points, dimension totals, and maturity thresholds synchronized across English, Brazilian Portuguese, Spanish, Simplified Chinese, and Hindi.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The latest version is &lt;strong&gt;1.6.4&lt;/strong&gt;. You can run the default audit without installing it in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--yes&lt;/span&gt; harness-score
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Harness Score is actually measuring
&lt;/h2&gt;

&lt;p&gt;The harness engineering discussion is broader than any single tool. &lt;a href="https://martinfowler.com/articles/harness-engineering.html" rel="noopener noreferrer"&gt;Birgitta Böckeler describes&lt;/a&gt; an outer harness that combines guidance with feedback sensors so agents can prevent mistakes and self-correct earlier. &lt;a href="https://www.langchain.com/blog/the-anatomy-of-an-agent-harness" rel="noopener noreferrer"&gt;LangChain's anatomy of an agent harness&lt;/a&gt; similarly separates the model from the surrounding prompts, tools, permissions, state, and execution infrastructure.&lt;/p&gt;

&lt;p&gt;Harness Score focuses that broad idea on evidence that can be found in a software repository. Its six dimensions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context &amp;amp; Guides:&lt;/strong&gt; whether the agent can understand the project and its conventions;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills &amp;amp; Commands:&lt;/strong&gt; whether repeated procedures are packaged and discoverable;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks &amp;amp; Guardrails:&lt;/strong&gt; whether important behavior is enforced during agent execution;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensors &amp;amp; Feedback:&lt;/strong&gt; whether tests, linting, types, formatting, and test files can check the work;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI Feedback:&lt;/strong&gt; whether those sensors run as part of the delivery workflow;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hygiene &amp;amp; Safety:&lt;/strong&gt; whether secrets, environment files, lockfiles, licenses, and MCP configuration are handled safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is not just a percentage. The maturity ladder distinguishes a repository that is merely documented from one that has feedback loops and enforced guardrails. Each failed check includes evidence and a remediation path in the &lt;a href="https://paladini.io/harness-score/guide/metrics-and-codes.html#check-catalog" rel="noopener noreferrer"&gt;complete check catalog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1.5.3: do not trust an incomplete scan
&lt;/h2&gt;

&lt;p&gt;The most important change in 1.5.3 was a safety decision: an incomplete repository walk must not look like a complete evaluation.&lt;/p&gt;

&lt;p&gt;Before this release, filesystem limits or unreadable paths could produce a partial view while still leaving a maturity result visible. Version 1.5.3 made incomplete repository and effective-scope walks &lt;strong&gt;fail closed&lt;/strong&gt;. Terminal output, Markdown reports, badges, diffs, the CLI exit status, and the GitHub Action all learned the difference between an authoritative result and provisional diagnostics.&lt;/p&gt;

&lt;p&gt;This matters because a high score based on only the easy-to-read part of a repository is worse than no score. It creates false confidence.&lt;/p&gt;

&lt;p&gt;The same release improved native tool-root support. If a supported configuration directory was itself the scan root, Harness Score became better at recognizing Claude Code, Cursor, Codex, Windsurf, Cline, Continue, Gemini, Zed, OpenCode, and GitHub harness layouts. Hook configuration selection also became deterministic, with updated Claude Code event and handler validation and forward-compatible warnings for structurally valid future events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1.6.0: complete repository scans at real-world scale
&lt;/h2&gt;

&lt;p&gt;Version 1.6.0 extended that fail-closed foundation. The production depth cap was removed, and the file-count safety fuse increased to &lt;strong&gt;1,000,000 files&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That change is especially useful for monorepos and deeply nested workspaces. A CI workflow, rule, test suite, or harness artifact should not disappear from the evaluation simply because it lives farther down the tree than expected.&lt;/p&gt;

&lt;p&gt;The scanner also began treating discovered paths that cannot be inspected and symlinks that escape the scan root as explicit incomplete-scan conditions. The important product behavior stayed the same: Harness Score can still explain what it found, but it will not pretend that a partial snapshot is an authoritative maturity verdict.&lt;/p&gt;

&lt;p&gt;No check IDs, points, thresholds, or public APIs changed in 1.6.0. The release improved the reliability of the measurement rather than changing the ruler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1.6.1: CI inside nested workspaces now counts
&lt;/h2&gt;

&lt;p&gt;Many repositories do not keep every CI file at the top level. A monorepo may contain independently deployed applications, each with its own Jenkinsfile, GitLab pipeline, Azure Pipelines configuration, CircleCI setup, or Bitbucket pipeline.&lt;/p&gt;

&lt;p&gt;Version 1.6.1 made non-GitHub CI discovery work at any depth below the scan root. It also stabilized evidence ordering and removed duplicate evidence when matcher patterns overlap.&lt;/p&gt;

&lt;p&gt;For users, the practical improvement is simple: you should not need to rearrange a legitimate workspace structure just to make the scanner understand that CI exists and runs tests or static checks.&lt;/p&gt;

&lt;p&gt;This release was also a good example of open-source feedback improving the product. Thanks to &lt;a href="https://github.com/felipecontratres-gupy" rel="noopener noreferrer"&gt;Felipe Contratres&lt;/a&gt; for reporting the nested-workspace problem and contributing the initial fix, and to &lt;a href="https://github.com/dbtorrico" rel="noopener noreferrer"&gt;dbtorrico&lt;/a&gt; for identifying a guide-locale parity gap and suggesting translations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1.6.2: Google Cloud Build support
&lt;/h2&gt;

&lt;p&gt;Version 1.6.2 added deterministic discovery for Google Cloud Build configurations under &lt;code&gt;cloudbuild/&lt;/code&gt; using &lt;code&gt;.yml&lt;/code&gt; or &lt;code&gt;.yaml&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Those files can now contribute evidence to the CI pipeline, test execution, and lint/type-check checks. The scanner remains filesystem-only: it confirms that the configuration is present and contains the relevant commands, but it does not call Google Cloud or claim that a remote pipeline actually ran.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://github.com/gabrielcaiana" rel="noopener noreferrer"&gt;Gabriel Caiana&lt;/a&gt; for reporting the gap and supplying a reproducible Cloud Build pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1.6.3: Forgejo Actions support
&lt;/h2&gt;

&lt;p&gt;Not every team hosts its repositories and automation on GitHub. Version 1.6.3 added support for Forgejo Actions workflows in &lt;code&gt;.forgejo/workflows/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That makes the CI dimension more useful for teams choosing a self-hosted, open-source forge. The same CI checks can recognize the workflow and its test and lint/type-check steps without adding a provider API, credentials, telemetry, or network behavior to the scanner.&lt;/p&gt;

&lt;p&gt;Taken together, versions 1.6.1 through 1.6.3 move Harness Score closer to its intended position: the repository structure and working controls should matter more than the vendor logo on the pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1.6.4: documentation that cannot silently drift
&lt;/h2&gt;

&lt;p&gt;The scanner already had the correct weights, but the compact check table in the documentation had five stale values. Version 1.6.4 corrected them in every locale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CI-03&lt;/code&gt;: 3 points;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CI-04&lt;/code&gt;: 3 points;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HYG-01&lt;/code&gt;: 2 points;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HYG-06&lt;/code&gt;: 2 points;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HYG-08&lt;/code&gt;: 3 points.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, the release expanded the synchronization tests. CI now verifies unique and complete table rows, the weights defined by the implementation, all six dimension totals, detailed catalog headings, the 108-point total, and the maturity thresholds across all five languages.&lt;/p&gt;

&lt;p&gt;This patch did &lt;strong&gt;not&lt;/strong&gt; change anyone's score. It aligned the public explanation with the implemented model and made the same class of drift fail CI in the future. The &lt;a href="https://github.com/paladini/harness-score/releases/tag/v1.6.4" rel="noopener noreferrer"&gt;1.6.4 release notes&lt;/a&gt; include the complete validation record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the score into a ratchet, not a one-time report
&lt;/h2&gt;

&lt;p&gt;A local scan is the fastest way to understand the project, but the score becomes more useful when it is part of the engineering loop.&lt;/p&gt;

&lt;p&gt;Save a machine-readable baseline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--yes&lt;/span&gt; harness-score &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; harness-score.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare a future scan against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--yes&lt;/span&gt; harness-score &lt;span class="nt"&gt;--diff&lt;/span&gt; harness-score.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or set a minimum maturity level in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--yes&lt;/span&gt; harness-score &lt;span class="nt"&gt;--min-level&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GitHub Action can run the same audit on every push or pull request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;paladini/harness-score@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;min-level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For stricter supply-chain control, pin the Action to a full commit SHA. For a quick adoption path, the stable &lt;code&gt;v1&lt;/code&gt; tag follows compatible 1.x releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the score does not prove
&lt;/h2&gt;

&lt;p&gt;Harness Score is an objective measure of &lt;strong&gt;repository-level harness infrastructure&lt;/strong&gt;, not a certification of final software quality.&lt;/p&gt;

&lt;p&gt;It can verify that tests, linters, type checking, CI workflows, context files, hooks, and safety patterns exist and are structurally connected. It does not prove that the tests assert the right behavior, that an &lt;code&gt;AGENTS.md&lt;/code&gt; is perfectly written, that a remote pipeline is healthy, or that an agent understood the product requirement.&lt;/p&gt;

&lt;p&gt;Use it as a readiness map and an improvement backlog. Combine it with code review, runtime observability, product evaluation, security testing, and semantic checks appropriate to your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is planned next
&lt;/h2&gt;

&lt;p&gt;The public &lt;a href="https://github.com/paladini/harness-score/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt; is a direction of travel, not a promise of dates or release numbers. The main planned areas are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;harness-score init&lt;/code&gt;:&lt;/strong&gt; deterministically scaffold starter artifacts for high-value missing checks, turning diagnosis into an optional, reviewable first fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SARIF output:&lt;/strong&gt; bring relevant findings into GitHub's code-scanning experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Larger real-world corpus studies:&lt;/strong&gt; use evidence from public repositories to challenge and calibrate thresholds, weights, and missing checks, then publish the findings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broader ecosystem detection:&lt;/strong&gt; recognize more test runners, linters, type checkers, and CI conventions as real repositories expose gaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More tool adapters:&lt;/strong&gt; continue the &lt;a href="https://github.com/paladini/harness-score/blob/main/PLUGINS-ROADMAP.md" rel="noopener noreferrer"&gt;multi-harness plugin plan&lt;/a&gt; beyond the current Cursor and Claude Code work, with lightweight integrations for Codex, OpenCode, Junie, Windsurf, Cline, Continue, and Zed before considering a heavier VS Code extension.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are also narrower reliability improvements under discussion and community review, including generated environment directories that should not invalidate a scan and better parity when a tool configuration directory is itself the repository root. These will only become release claims after implementation, review, and the same cross-platform gates used by the current release train.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the latest release
&lt;/h2&gt;

&lt;p&gt;Run this from the root of a repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--yes&lt;/span&gt; harness-score
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then look at the first failed checks and ask a practical question: which missing artifact would reduce the most repeated agent mistake in this project?&lt;/p&gt;

&lt;p&gt;You can explore the &lt;a href="https://paladini.io/harness-score/" rel="noopener noreferrer"&gt;guide&lt;/a&gt;, inspect the &lt;a href="https://github.com/paladini/harness-score/blob/main/packages/cli/CHANGELOG.md" rel="noopener noreferrer"&gt;full changelog&lt;/a&gt;, or contribute a detector and reproducible fixture through the &lt;a href="https://github.com/paladini/harness-score" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article was prepared with AI assistance. Release claims, commands, roadmap language, tags, links, and the current 1.6.4 package metadata were checked against the public repository, documentation, DEV Community, and package registry before publication.&lt;/p&gt;

&lt;p&gt;Which part of your AI coding harness is hardest to make visible today: context, feedback, guardrails, or CI?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Validate Card Brands in Node.js with Luhn and credit-card-brand-detector</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Sat, 05 Sep 2026 12:36:38 +0000</pubDate>
      <link>https://dev.to/paladini/validate-card-brands-in-nodejs-with-luhn-and-credit-card-brand-detector-1pip</link>
      <guid>https://dev.to/paladini/validate-card-brands-in-nodejs-with-luhn-and-credit-card-brand-detector-1pip</guid>
      <description>&lt;p&gt;When a checkout form receives a card number, the first useful question is often not whether the payment will be approved. It is whether the input is structurally plausible and which network rules should be shown to the user.&lt;/p&gt;

&lt;p&gt;The open-source &lt;a href="https://github.com/paladini/credit-card-brand-detector" rel="noopener noreferrer"&gt;&lt;code&gt;credit-card-brand-detector&lt;/code&gt;&lt;/a&gt; package provides that small client-side or server-side building block. It detects 11 brands, removes spaces and hyphens, and applies a Luhn checksum. It has zero runtime dependencies and exposes CommonJS functions for validation and brand detection.&lt;/p&gt;

&lt;p&gt;This tutorial builds a minimal Node.js check, verifies the result with known test numbers, and explains what this kind of validation cannot tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install version &lt;code&gt;1.0.1&lt;/code&gt;, call &lt;code&gt;validateCreditCard&lt;/code&gt; when you need both a boolean result and a brand, and call &lt;code&gt;detectBrand&lt;/code&gt; when you only need the network name. The package does not contact a payment processor, authorize a transaction, tokenize data, or prove that a card exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 12 or newer. The package declares &lt;code&gt;&amp;gt;=12.0.0&lt;/code&gt; in its metadata.&lt;/li&gt;
&lt;li&gt;npm.&lt;/li&gt;
&lt;li&gt;A terminal and a small JavaScript file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package is released under the &lt;a href="https://github.com/paladini/credit-card-brand-detector/blob/main/LICENSE" rel="noopener noreferrer"&gt;MIT license&lt;/a&gt;. The examples below target the published npm package version &lt;code&gt;1.0.1&lt;/code&gt;, which is also the version I installed for this walkthrough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the package
&lt;/h2&gt;

&lt;p&gt;Create a directory for the example and install the pinned version:&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;mkdir &lt;/span&gt;card-check-example
&lt;span class="nb"&gt;cd &lt;/span&gt;card-check-example
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;credit-card-brand-detector@1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pinning the version makes the example reproducible. If you use a different version later, check its README and package metadata before copying the behavior into a production application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the smallest useful check
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;check-card.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;validateCreditCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;detectBrand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getBrand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;credit-card-brand-detector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedVisa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4532 0151-1283-0366&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mastercard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;5555555555554444&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;validateCreditCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formattedVisa&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;detectBrand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mastercard&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getBrand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mastercard&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node check-card.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ isValid: true, bandeira: 'Visa' }
Mastercard
Mastercard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Portuguese property name &lt;code&gt;bandeira&lt;/code&gt; is part of the package's public return shape. Keep it as-is when consuming the API, or map it to an application-specific name at your boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understand the two API paths
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;validateCreditCard(cardNumber)&lt;/code&gt; returns an object with &lt;code&gt;isValid&lt;/code&gt; and &lt;code&gt;bandeira&lt;/code&gt;. It first strips non-digit characters, rejects values shorter than 13 or longer than 19 digits, detects a brand from configured prefixes, and then runs the Luhn calculation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;detectBrand(cardNumber)&lt;/code&gt; returns a brand name or &lt;code&gt;null&lt;/code&gt;. &lt;code&gt;getBrand&lt;/code&gt; is an alias for the same function. This is useful when the UI needs to change an icon or helper message before the complete number is validated.&lt;/p&gt;

&lt;p&gt;The implementation checks more specific ranges before broad ones. That matters for Brazilian brands such as Elo, whose configured prefixes can begin with &lt;code&gt;4&lt;/code&gt;, the same first digit used by Visa. A broad prefix check performed first could classify a supported Elo example as Visa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an explicit application boundary
&lt;/h2&gt;

&lt;p&gt;The package accepts a string and normalizes it internally, but your application should decide how to handle empty input, pasted content, and form errors. A small wrapper can keep the package's output separate from UI messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;validateCreditCard&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;credit-card-brand-detector&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;inspectCardInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;''&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter a card number.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validateCreditCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bandeira&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The card network is not recognized.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isValid&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The number failed its checksum.&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bandeira&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;inspectCardInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4532015112830366&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This wrapper deliberately reports a checksum failure as an input problem. It does not imply that a valid checksum means the card can be charged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the verification
&lt;/h2&gt;

&lt;p&gt;The repository README documents these public examples. You can check the package independently with a one-line Node command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"const p=require('credit-card-brand-detector'); console.log(JSON.stringify({validVisa:p.validateCreditCard('4532015112830366'),mastercard:p.detectBrand('5555555555554444'),formatted:p.validateCreditCard('4532 0151-1283-0366'),unknown:p.detectBrand('1234567890123')}))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verified result is:&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="nl"&gt;"validVisa"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"isValid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"bandeira"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Visa"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"mastercard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Mastercard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"formatted"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"isValid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"bandeira"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Visa"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;null&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;For an application test suite, add cases for formatting normalization, unknown prefixes, too-short values, each supported network you rely on, and known invalid checksums. Do not use real customer card numbers in tests or fixtures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the check works
&lt;/h2&gt;

&lt;p&gt;Brand detection is prefix matching against the rules in the package source. Luhn validation is a checksum calculation that doubles alternating digits from the right, subtracts nine when a doubled value exceeds nine, and checks whether the total is divisible by ten.&lt;/p&gt;

&lt;p&gt;That division of responsibility is useful: prefix rules answer “which configured network might this resemble?” while Luhn answers “does this string satisfy the checksum?” Neither step performs authorization or a network lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and security boundaries
&lt;/h2&gt;

&lt;p&gt;There are several important limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A valid Luhn result does not prove that a card is issued, active, funded, or owned by the person entering it.&lt;/li&gt;
&lt;li&gt;Prefix tables can become outdated as networks change ranges. Review the package source and release history before relying on a classification for business logic.&lt;/li&gt;
&lt;li&gt;The package strips non-digit characters. That is convenient for spaces and hyphens, but it should not replace input limits, rate limiting, or server-side validation.&lt;/li&gt;
&lt;li&gt;Never log full card numbers. Avoid putting them in analytics events, exception messages, URLs, screenshots, or support tickets.&lt;/li&gt;
&lt;li&gt;Browser-side validation is only a user-experience aid. Payment credentials should be handled through a PCI-compliant payment provider and tokenization flow appropriate to your system.&lt;/li&gt;
&lt;li&gt;This package has no payment gateway integration and makes no security guarantee for an application that uses it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use test numbers supplied by your payment provider for payment-flow tests. For a real checkout, send payment data only through the provider's documented secure collection mechanism instead of building your own storage path.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this package charge a card?
&lt;/h3&gt;

&lt;p&gt;No. It only analyzes a string locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a valid result mean the payment will succeed?
&lt;/h3&gt;

&lt;p&gt;No. Authorization, funds, fraud checks, expiration, and issuer decisions happen elsewhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use it with formatted input?
&lt;/h3&gt;

&lt;p&gt;Yes. The implementation removes non-digit characters before checking length, prefixes, and the checksum.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is the result property named &lt;code&gt;bandeira&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;That is the package's documented public API. Map it in your own code if your application uses English field names.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it support every card network?
&lt;/h3&gt;

&lt;p&gt;No. The README lists 11 supported brands. Treat &lt;code&gt;null&lt;/code&gt; as “not recognized by this rule set,” not as proof that a number is invalid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;credit-card-brand-detector&lt;/code&gt; is a focused Node.js utility for early input feedback. Its useful boundary is narrow: normalize a candidate number, identify a configured brand, and run a checksum before the payment workflow begins. Keep that result separate from authorization, and keep payment data out of logs and application storage.&lt;/p&gt;

&lt;p&gt;This article was prepared with AI assistance. The commands, package version, API behavior, and example output were checked against the public repository and the installed npm package before publication.&lt;/p&gt;

&lt;p&gt;What additional boundary would you test before putting a card-number helper in front of a payment provider?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>npm</category>
      <category>tutorial</category>
      <category>validation</category>
    </item>
    <item>
      <title>Keep Rails Design Tokens Reviewable with rails-design-profiles</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Fri, 04 Sep 2026 12:37:30 +0000</pubDate>
      <link>https://dev.to/paladini/keep-rails-design-tokens-reviewable-with-rails-design-profiles-311f</link>
      <guid>https://dev.to/paladini/keep-rails-design-tokens-reviewable-with-rails-design-profiles-311f</guid>
      <description>&lt;p&gt;Design references are useful until they start changing production code by implication.&lt;/p&gt;

&lt;p&gt;An AI assistant or a contributor can read a &lt;code&gt;DESIGN.md&lt;/code&gt; file and understand a visual direction. That does not mean the application should execute the file, copy an entire stylesheet, or silently adopt a third-party brand. The safer boundary is to review the reference, approve a small set of values, and commit those values as an explicit application contract.&lt;/p&gt;

&lt;p&gt;This tutorial shows that workflow with &lt;a href="https://github.com/paladini/rails-design-profiles" rel="noopener noreferrer"&gt;&lt;code&gt;rails-design-profiles&lt;/code&gt;&lt;/a&gt;, a MIT-licensed Ruby gem for Rails. The stable &lt;code&gt;v0.1.2&lt;/code&gt; release stores profiles in YAML and emits the active profile as &lt;code&gt;--rdp-*&lt;/code&gt; CSS custom properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Install the gem, run its generator, add a profile to &lt;code&gt;config/design_profiles.yml&lt;/code&gt;, and include &lt;code&gt;&amp;lt;%= rails_design_profiles_tags %&amp;gt;&lt;/code&gt; in your layout. Rails then emits a small style tag containing only valid token names and safe token values. Your existing CSS decides how to use those variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ruby 3.1 or newer.&lt;/li&gt;
&lt;li&gt;Rails 7.1 or newer.&lt;/li&gt;
&lt;li&gt;A Rails application where you can edit the Gemfile and application layout.&lt;/li&gt;
&lt;li&gt;A CSS setup that can consume CSS custom properties.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gem declares &lt;code&gt;actionview &amp;gt;= 7.1&lt;/code&gt; and &lt;code&gt;railties &amp;gt;= 7.1&lt;/code&gt; as dependencies. Its gemspec identifies the release as version &lt;code&gt;0.1.2&lt;/code&gt; and the license as MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the stable release
&lt;/h2&gt;

&lt;p&gt;Add the gem to your Rails application's Gemfile:&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;gem&lt;/span&gt; &lt;span class="s2"&gt;"rails-design-profiles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"0.1.2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies and run the generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;bin/rails generate rails_design_profiles:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator creates &lt;code&gt;config/design_profiles.yml&lt;/code&gt; and adds this helper to the application layout's &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;rails_design_profiles_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The helper is intentionally small. It reads the active profile, converts accepted tokens into CSS custom properties, and renders one style tag. It does not replace your stylesheet or select a CSS framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define an explicit profile
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;config/design_profiles.yml&lt;/code&gt;. The documented shape is a selected profile plus a map of named profiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;editorial&lt;/span&gt;
&lt;span class="na"&gt;profiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;editorial&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;reference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config/design_profiles/references/claude.md&lt;/span&gt;
    &lt;span class="na"&gt;tokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;color-primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#c15f3c"&lt;/span&gt;
      &lt;span class="na"&gt;color-surface&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#f8f6f2"&lt;/span&gt;
      &lt;span class="na"&gt;color-text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#24211e"&lt;/span&gt;
      &lt;span class="na"&gt;font-body&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Newsreader,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Georgia,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;serif"&lt;/span&gt;
      &lt;span class="na"&gt;space-page&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.5rem"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;reference&lt;/code&gt; field documents where the human-readable context came from. It is metadata for people and tools. The &lt;code&gt;tokens&lt;/code&gt; map is the production contract.&lt;/p&gt;

&lt;p&gt;Use those variables from the CSS already in your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rdp-color-text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rdp-color-surface&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rdp-font-body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rdp-color-primary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rdp-space-page&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;p&gt;The gem changes a token name such as &lt;code&gt;color-primary&lt;/code&gt; into &lt;code&gt;--rdp-color-primary&lt;/code&gt;. The prefix keeps these values easy to locate and reduces accidental collisions with unrelated custom properties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import a reference without making it executable
&lt;/h2&gt;

&lt;p&gt;Version 0.1 supports the MIT-licensed &lt;a href="https://github.com/VoltAgent/awesome-design-md" rel="noopener noreferrer"&gt;&lt;code&gt;VoltAgent/awesome-design-md&lt;/code&gt;&lt;/a&gt; catalog. List its available references:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails design:list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install a reference into your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails design:install[claude]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task archives the reference at &lt;code&gt;config/design_profiles/references/claude.md&lt;/code&gt; and creates an empty &lt;code&gt;claude&lt;/code&gt; profile. Read it, discuss which ideas fit your product, and add only the tokens your team approves. Existing references and profiles are not overwritten.&lt;/p&gt;

&lt;p&gt;This is an important security and maintenance boundary. The catalog file is content to inspect, not Ruby code to load. The gem downloads it over HTTPS, checks the response status, validates the slug shape, and writes it under the application's configuration directory.&lt;/p&gt;

&lt;p&gt;The project also states that it is not affiliated with getdesign.md, VoltAgent, or any referenced brand. Treat an imported reference as an input to review, not as an endorsement or a ready-made theme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switch the active profile
&lt;/h2&gt;

&lt;p&gt;After defining another profile, activate it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails design:activate[editorial]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task updates the &lt;code&gt;active&lt;/code&gt; key in &lt;code&gt;config/design_profiles.yml&lt;/code&gt;. The new application-wide profile is used on the next response. Because the change is a normal configuration diff, a reviewer can see which values changed before deployment.&lt;/p&gt;

&lt;p&gt;Per-session previews and query-string overrides are intentionally outside the first release. If you need a live theme picker, build that as a separate feature with its own authorization and caching design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the result
&lt;/h2&gt;

&lt;p&gt;There are two useful checks. First, inspect the rendered HTML in a development response. You should find a style tag similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"rails-design-profiles"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="py"&gt;--rdp-color-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#c15f3c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="py"&gt;--rdp-color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f8f6f2&lt;/span&gt;&lt;span class="p"&gt;;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, verify that the CSS uses the variables rather than hard-coded replacements. A browser's computed-style panel should show the value flowing from the custom property into the component.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;v0.1.2&lt;/code&gt; test suite covers seven assertions across profile rendering, missing profiles, invalid YAML, reference installation, invalid slugs, activation, and helper output. In a clean checkout, the two Minitest files ran with 7 tests and 9 assertions, with zero failures or errors. The documented gem build also succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;The design decision is separation of concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;DESIGN.md&lt;/code&gt; gives humans and AI tools context about a visual direction.&lt;/li&gt;
&lt;li&gt;A reviewed YAML profile records the subset that the product actually accepts.&lt;/li&gt;
&lt;li&gt;The helper exposes those values through standard CSS primitives.&lt;/li&gt;
&lt;li&gt;Existing components remain responsible for layout, interaction, and accessibility.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That separation makes changes reversible. Removing a token or changing the active profile is a small diff. It also avoids pretending that prose can safely compile an entire interface without review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes and limitations
&lt;/h2&gt;

&lt;p&gt;An unknown active profile raises &lt;code&gt;ProfileNotFound&lt;/code&gt;. Invalid YAML raises &lt;code&gt;ConfigurationError&lt;/code&gt;, as does a missing or malformed profiles mapping. A token is ignored unless its name uses lowercase letters, numbers, and hyphens, and its value matches the gem's conservative character allowlist. This filtering is helpful, but it is not a complete CSS sanitizer or a substitute for reviewing configuration changes.&lt;/p&gt;

&lt;p&gt;The active profile is global to the Rails application and evaluated for each response. The first release does not provide user-specific themes, preview sessions, token validation diagnostics, or adapters for arbitrary public catalogs. The README lists those as planned work, so do not present them as current features.&lt;/p&gt;

&lt;p&gt;The catalog download is a network dependency. Pin the gem version, review imported files, and commit the archived reference if reproducibility matters. Do not put secrets, user-provided CSS, or untrusted dynamic values into the profile. Keep the helper output under normal Rails content-security-policy and caching review.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this replace Tailwind or the asset pipeline?
&lt;/h3&gt;

&lt;p&gt;No. The gem emits CSS custom properties that can be consumed by the asset pipeline, Propshaft, Importmap, Tailwind, or custom CSS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does importing a reference install its branding?
&lt;/h3&gt;

&lt;p&gt;No. The reference is archived as Markdown and the profile starts empty. Your team chooses the production tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I override the profile with a query parameter?
&lt;/h3&gt;

&lt;p&gt;Not in the first release. Profile switching is an explicit application-wide task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the gem a design-system validator?
&lt;/h3&gt;

&lt;p&gt;No. It provides a deliberately small bridge from reviewed configuration to CSS variables. The release roadmap mentions validation and diagnostics as future work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rails-design-profiles&lt;/code&gt; is useful when you want design context to help Rails work without letting reference prose or copied CSS become an invisible production dependency. Keep the reference readable, keep the approved tokens in version control, and let your existing CSS consume the explicit &lt;code&gt;--rdp-*&lt;/code&gt; contract.&lt;/p&gt;

&lt;p&gt;What review rule would your team require before a design reference is allowed to influence production tokens?&lt;/p&gt;

&lt;p&gt;Disclosure: AI assistance was used to organize and edit this tutorial. The project behavior, release metadata, source excerpts, and test results were checked against the public repository and stable &lt;code&gt;v0.1.2&lt;/code&gt; tag.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tutorial</category>
      <category>designtokens</category>
    </item>
    <item>
      <title>Plan a Trekking Trip With AI, a Weight Spreadsheet, and the LighterPack SDK</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Thu, 03 Sep 2026 13:40:22 +0000</pubDate>
      <link>https://dev.to/paladini/plan-a-trekking-trip-with-ai-a-weight-spreadsheet-and-the-lighterpack-sdk-k7j</link>
      <guid>https://dev.to/paladini/plan-a-trekking-trip-with-ai-a-weight-spreadsheet-and-the-lighterpack-sdk-k7j</guid>
      <description>&lt;p&gt;Planning a multi-day trek is a data problem disguised as an adventure. You collect GPX files, weather notes, permit links, and a growing pile of gear receipts. Weight limits show up in airline rules, in your own fitness goals, and in every "do I really need this?" moment at 4,000 meters.&lt;/p&gt;

&lt;p&gt;Most hikers end up with the same fragmentation: a spreadsheet here, a LighterPack list there, product links in browser tabs, and half-remembered weights copied from store pages. The list gets stale the moment you buy one more layer or swap a stove.&lt;/p&gt;

&lt;p&gt;This tutorial describes a workflow I used while preparing a Salkantay trek repository, generalized so you can adapt it to any hike. The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use AI to help you &lt;strong&gt;plan&lt;/strong&gt; the trek and normalize messy purchase data.&lt;/li&gt;
&lt;li&gt;Store the result in a &lt;strong&gt;CSV&lt;/strong&gt; that acts as the source of truth for weight, price, and status.&lt;/li&gt;
&lt;li&gt;Sync that CSV to &lt;strong&gt;LighterPack&lt;/strong&gt; with &lt;a href="https://www.npmjs.com/package/@paladini/lighterpack" rel="noopener noreferrer"&gt;&lt;code&gt;@paladini/lighterpack&lt;/code&gt;&lt;/a&gt;, an unofficial TypeScript SDK for &lt;a href="https://lighterpack.com" rel="noopener noreferrer"&gt;LighterPack&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will not get a magic "upload PDF → perfect pack list" button. You will get a repeatable pipeline you can version in Git, re-run before a trip, and extend with your own rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are building
&lt;/h2&gt;

&lt;p&gt;Think in three layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Planning&lt;/td&gt;
&lt;td&gt;Context, route, references&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;README.md&lt;/code&gt;, GPX, notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory&lt;/td&gt;
&lt;td&gt;Quantities, weights, prices, purchase status&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gear/items.csv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Publishing&lt;/td&gt;
&lt;td&gt;Shareable pack list with categories and photos&lt;/td&gt;
&lt;td&gt;LighterPack list + public link&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AI sits on the &lt;strong&gt;edges&lt;/strong&gt; of this system: helping you draft the plan, extracting rows from invoices, and filling gaps when a product page is hard to parse. The CSV stays deterministic. The sync script stays boring. That separation is what makes the workflow trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://lighterpack.com" rel="noopener noreferrer"&gt;LighterPack&lt;/a&gt; account (the service is &lt;a href="https://github.com/galenmaly/lighterpack" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and can be self-hosted).&lt;/li&gt;
&lt;li&gt;Node.js 20+ for the sync script.&lt;/li&gt;
&lt;li&gt;A folder per trek, for example &lt;code&gt;2026/my-trek/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;An AI assistant (chat or agent) for unstructured inputs: notes, invoices, screenshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SDK is &lt;strong&gt;unofficial&lt;/strong&gt;. It talks to the same web API the LighterPack site uses and authenticates with your normal username and password. Treat those credentials like any other secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Let AI help you plan the trek (without owning the truth)
&lt;/h2&gt;

&lt;p&gt;Start with questions AI is good at when you feed it structured context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Route length, elevation, season, and resupply points.&lt;/li&gt;
&lt;li&gt;Group size and shared vs personal gear.&lt;/li&gt;
&lt;li&gt;Constraints: vegan food, carry-on-only flights, rental vs owned items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ask for a &lt;strong&gt;draft&lt;/strong&gt; checklist by category (shelter, cooking, clothing, safety), not a final weight table. Your job is to reject, merge, and annotate.&lt;/p&gt;

&lt;p&gt;A useful prompt pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I am hiking [trail] in [month] with [N] people.
Constraints: [weight limit], [diet], [owned vs to-buy].
Return a table with columns: category, item, model, quantity, notes.
Mark each row as owned, to-buy, or borrow.
Do not invent weights or prices.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the narrative plan in &lt;code&gt;README.md&lt;/code&gt; and keep evolving it. Do &lt;strong&gt;not&lt;/strong&gt; let the chat transcript become your inventory database. Chats are great for exploration; they are poor as source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Turn invoices and receipts into spreadsheet rows
&lt;/h2&gt;

&lt;p&gt;This is where AI saves the most tedious work. After you buy gear, you usually have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PDF or email invoices.&lt;/li&gt;
&lt;li&gt;Marketplace order pages (Decathlon, AliExpress, Amazon, and regional stores).&lt;/li&gt;
&lt;li&gt;Screenshots with SKU, quantity, and price.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste the text (or attach a readable PDF/image) and ask the model to emit &lt;strong&gt;CSV-shaped rows&lt;/strong&gt; aligned with your schema.&lt;/p&gt;

&lt;p&gt;Recommended columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;category,item,model,quantity,unit_price,total_price,unit_weight_kg,total_weight_kg,baggage,status,notes,url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract gear purchases from this invoice into CSV rows.
Schema: category,item,model,quantity,unit_price,total_price,unit_weight_kg,total_weight_kg,baggage,status,notes,url
Rules:
- baggage is "carry-on" or "checked" when known, else empty.
- status is "owned", "to-buy", or "rent".
- unit_weight_kg only if explicitly stated; otherwise leave blank.
- url: product page if visible, else empty.
- One row per line item; quote fields that contain commas.
Output only CSV, no commentary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Review every row. AI will misread bundles ("2 pairs of socks" vs quantity 2), confuse shipping with product weight, and hallucinate URLs. Weights from manufacturer pages are often missing on invoices — look them up once, then store them in the CSV.&lt;/p&gt;

&lt;p&gt;For a two-person trip, decide early whether quantities are &lt;strong&gt;per person&lt;/strong&gt; or &lt;strong&gt;shared&lt;/strong&gt; and document that in &lt;code&gt;notes&lt;/code&gt;. LighterPack supports quantity per line item; your spreadsheet should match how you think about the pack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Treat the CSV as the contract
&lt;/h2&gt;

&lt;p&gt;Once reviewed, commit &lt;code&gt;gear/items.csv&lt;/code&gt; to Git (or keep it locally if you prefer). This file is the contract between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your planning spreadsheet,&lt;/li&gt;
&lt;li&gt;your automation script,&lt;/li&gt;
&lt;li&gt;and the published LighterPack list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conventions that paid off in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Categories&lt;/strong&gt; map directly to LighterPack categories (&lt;code&gt;Camping&lt;/code&gt;, &lt;code&gt;Clothing&lt;/code&gt;, &lt;code&gt;Hydration&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weights&lt;/strong&gt; in kilograms in the CSV; convert to grams in code (&lt;code&gt;Math.round(kg * 1000)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worn&lt;/strong&gt; and &lt;strong&gt;consumable&lt;/strong&gt; flags are derived with small keyword lists (boots, poles, freeze-dried meals) instead of extra columns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product URLs&lt;/strong&gt; enable image resolution later; empty URLs are fine for generic items.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal row might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clothing,Trekking boots,Hoka Speedgoat 6,2,1000.0,2000,0.267,0.534,carry-on,owned,,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4 — Sync to LighterPack with TypeScript
&lt;/h2&gt;

&lt;p&gt;Install the SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @paladini/lighterpack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;scripts/lighterpack/.env&lt;/code&gt; (never commit it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIGHTERPACK_USERNAME=your_username
LIGHTERPACK_PASSWORD=your_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sync script reads the CSV, groups items by category, resolves images, and creates the list in one batch call. The core pattern:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LighterPackClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@paladini/lighterpack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LighterPackClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LIGHTERPACK_USERNAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LIGHTERPACK_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setCurrencySymbol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// or €, £, etc.&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createListWithItems&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;My Trek 2026 (2 people)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Generated from gear/items.csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;categories&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Shelter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2-person tent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Naturehike Star River 2 · Checked bag&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;qty&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="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1950&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// grams&lt;/span&gt;
          &lt;span class="na"&gt;weightUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/tent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;worn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;consumable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOptionalFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;worn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;consumable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;packWeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shareUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateShareLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shareUrl&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;createListWithItems&lt;/code&gt; is the workhorse: one round trip to scaffold categories and items. For large lists (50+ lines), this is far less fragile than clicking through the UI.&lt;/p&gt;

&lt;p&gt;Map CSV columns to SDK fields explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; ← &lt;code&gt;item&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt; ← &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;notes&lt;/code&gt;, &lt;code&gt;baggage&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; (joined as readable text)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qty&lt;/code&gt; ← &lt;code&gt;quantity&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weight&lt;/code&gt; ← &lt;code&gt;unit_weight_kg&lt;/code&gt; converted to grams&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt; ← &lt;code&gt;unit_price&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;url&lt;/code&gt; ← &lt;code&gt;url&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5 — Product images (expect breakage)
&lt;/h2&gt;

&lt;p&gt;LighterPack can display item photos from external URLs, but retailer CDNs often block hotlinking or return generic Open Graph images. In a real sync of ~50 items, a majority of naive &lt;code&gt;og:image&lt;/code&gt; URLs failed.&lt;/p&gt;

&lt;p&gt;A more reliable approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Resolve image URLs per product (Decathlon VTEX packshots, AliExpress media CDN, manufacturer sites).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download&lt;/strong&gt; images locally with a small Python script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload&lt;/strong&gt; with &lt;code&gt;lp.items.uploadImage(itemId, { buffer, filename, mimeType })&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Local upload beat &lt;code&gt;setImageUrl&lt;/code&gt; for visibility on the share page. Keep downloaded binaries out of Git; store only the resolver map and scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;After &lt;code&gt;npm run sync:trek&lt;/code&gt; (or your own script name), check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Totals&lt;/strong&gt; — LighterPack pack weight and price should match your spreadsheet sums within rounding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categories&lt;/strong&gt; — item counts per category match CSV grouping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags&lt;/strong&gt; — boots and poles marked worn; meals marked consumable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share link&lt;/strong&gt; — open the generated URL in a private window.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example output from a successful run:&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;"listId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Salkantay 2026 (2 people)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"weightGrams"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14489.78&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shareUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://lighterpack.com/r/example"&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;Re-run the sync after CSV edits. Idempotency strategy: find an existing list by name, delete it, recreate. Crude but clear for personal repos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security and boundaries
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never commit &lt;code&gt;.env&lt;/code&gt;, passwords, or invoice PDFs with personal data.&lt;/li&gt;
&lt;li&gt;Rotate your LighterPack password if it was ever pasted into a chat or log.&lt;/li&gt;
&lt;li&gt;The SDK session is in-memory; changing your account password revokes access.&lt;/li&gt;
&lt;li&gt;AI-generated weights and prices are &lt;strong&gt;suggestions&lt;/strong&gt; until you verify them against specs or a scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tradeoffs and limits
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CSV as source of truth&lt;/td&gt;
&lt;td&gt;Git-diffable, scriptable&lt;/td&gt;
&lt;td&gt;Manual review after AI extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch SDK create&lt;/td&gt;
&lt;td&gt;Fast, repeatable&lt;/td&gt;
&lt;td&gt;Deletes/recreates list on full sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI invoice parsing&lt;/td&gt;
&lt;td&gt;Less typing&lt;/td&gt;
&lt;td&gt;Requires validation; not fully autonomous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local image upload&lt;/td&gt;
&lt;td&gt;Reliable thumbnails&lt;/td&gt;
&lt;td&gt;Extra download/upload step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This workflow is optimized for &lt;strong&gt;multi-day treks&lt;/strong&gt; where gear lists are large and shared. For a day hike, a single LighterPack list edited by hand is probably enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Add a &lt;code&gt;make check&lt;/code&gt; that validates CSV totals and required fields before sync.&lt;/li&gt;
&lt;li&gt;Store trek metadata (dates, group size) beside the CSV for richer list descriptions.&lt;/li&gt;
&lt;li&gt;Wire the MCP server from the same SDK family if you want an agent to query the list without running scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try this pipeline, start with ten items end to end before importing a full expedition list. Fix the schema once, then scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question for readers:&lt;/strong&gt; Where do you draw the line between "AI drafts the row" and "I require a photo of the scale / spec sheet before it enters the CSV"? I still hand-verify every weight that affects carry-on compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://lighterpack.com" rel="noopener noreferrer"&gt;LighterPack&lt;/a&gt; — pack list service.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/galenmaly/lighterpack" rel="noopener noreferrer"&gt;LighterPack (open source)&lt;/a&gt; — self-hosting option.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/@paladini/lighterpack" rel="noopener noreferrer"&gt;&lt;code&gt;@paladini/lighterpack&lt;/code&gt; on npm&lt;/a&gt; — TypeScript SDK used in this workflow.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/paladini/lighterpack-sdk" rel="noopener noreferrer"&gt;lighterpack-sdk on GitHub&lt;/a&gt; — SDK source and API reference.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Restore Sanitized MCP Configurations Without Committing Secrets</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Thu, 03 Sep 2026 12:35:25 +0000</pubDate>
      <link>https://dev.to/paladini/restore-sanitized-mcp-configurations-without-committing-secrets-lj6</link>
      <guid>https://dev.to/paladini/restore-sanitized-mcp-configurations-without-committing-secrets-lj6</guid>
      <description>&lt;p&gt;AI assistants are useful only when their tools are configured consistently. That consistency becomes risky when a client configuration contains an API key, a browser profile, cookies, or a copied storage state file.&lt;/p&gt;

&lt;p&gt;The practical problem is not just "where do I put the MCP server?" It is how to share a reproducible setup without turning a public repository into a credential container.&lt;/p&gt;

&lt;p&gt;This tutorial uses &lt;a href="https://github.com/paladini/agent-skills" rel="noopener noreferrer"&gt;paladini/agent-skills&lt;/a&gt; to restore sanitized MCP client examples. You will clone the repository, inspect its placeholders, copy one configuration locally, and run the repository's deterministic checks. The same workflow applies to other public configuration examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the repository provides
&lt;/h2&gt;

&lt;p&gt;The repository collects reusable Agent Skills and sanitized configurations for DEV.to, Medium, and LinkedIn workflows. Its README says the configuration examples contain placeholders rather than real credentials, and its publishing guardrails keep the final public action supervised.&lt;/p&gt;

&lt;p&gt;The useful design decision is the separation between a shareable shape and a local secret. A repository can document the command, arguments, environment-variable name, and publication boundary. It should not contain the value of &lt;code&gt;DEV_TO_API_KEY&lt;/code&gt;, a password, a cookie, an MFA code, or a browser storage state.&lt;/p&gt;

&lt;p&gt;This is also consistent with how MCP stdio transport works: the client launches the server as a subprocess and communicates over standard input and output. The client therefore needs a local command and local environment, but those values do not need to be committed to Git. See the &lt;a href="https://modelcontextprotocol.io/specification/2024-11-05/basic/transports" rel="noopener noreferrer"&gt;MCP transport specification&lt;/a&gt; for the transport behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git;&lt;/li&gt;
&lt;li&gt;Python 3.11 or newer for the bundled validation scripts;&lt;/li&gt;
&lt;li&gt;an MCP-compatible client if you want to connect a server after validation;&lt;/li&gt;
&lt;li&gt;a local directory outside the repository for real secrets and persistent browser profiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checkout used for this tutorial was the repository's &lt;code&gt;main&lt;/code&gt; branch at commit &lt;code&gt;f54fb0256aaf0d527f52d140a1c9996ea6925d26&lt;/code&gt;, dated 2026-07-21. The repository declares the MIT license in &lt;code&gt;LICENSE&lt;/code&gt;. There is no package installation step for the configuration collection itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clone and inspect before copying
&lt;/h2&gt;

&lt;p&gt;Start with a disposable checkout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/paladini/agent-skills.git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Set-Location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;agent-skills&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"%H %cs"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mcp-configs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important files for a DEV.to setup are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mcp-configs/
  devto/
    .env.example
    client.json.example
    codex.toml.example
  publishing-guardrails.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the example before replacing anything. The current TOML example contains a deliberately invalid executable path and a placeholder secret:&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;[mcp_servers.devto]&lt;/span&gt;
&lt;span class="py"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;PATH&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;TO&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;uv.exe"&lt;/span&gt;
&lt;span class="py"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"--directory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;PATH&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;TO&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s"&gt;devto-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"server.py"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[mcp_servers.devto.env]&lt;/span&gt;
&lt;span class="py"&gt;DEV_TO_API_KEY&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;local&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a template, not a configuration you can run unchanged. Replace the paths in your local client configuration only. Keep the committed example unchanged so another person can understand which values are required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restore a local configuration safely
&lt;/h2&gt;

&lt;p&gt;On Windows, copy the example to the local Codex configuration file, then edit the local copy. The repository's README recommends merging the table into &lt;code&gt;%USERPROFILE%\.codex\config.toml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Resolve-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".codex\config.toml"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Copy-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;\mcp-configs\devto\codex.toml.example"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="s2"&gt;.devto-example"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;notepad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="s2"&gt;.devto-example"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use an absolute path to the MCP server checkout and the &lt;code&gt;uv.exe&lt;/code&gt; executable on your own machine. Put the real key in the client-managed environment or secret store. Do not paste it into the repository file, the example file, a shell history entry, or a tutorial.&lt;/p&gt;

&lt;p&gt;If your client accepts JSON instead of TOML, the repository also provides &lt;code&gt;client.json.example&lt;/code&gt; with the same structure. The two formats are alternatives. Do not register both unless you intentionally want two server entries.&lt;/p&gt;

&lt;p&gt;After editing the real local configuration, verify that the file is outside the Git repository and that the placeholder is not being used accidentally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;USERPROFILE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".codex\config.toml"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Resolve-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp_servers|DEV_TO_API_KEY|REPLACE_WITH_LOCAL_SECRET"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--short&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last command should show no change caused by the local secret configuration. The &lt;code&gt;Resolve-Path&lt;/code&gt; result should point to your user configuration directory, not the cloned repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the public examples
&lt;/h2&gt;

&lt;p&gt;The repository includes a validator for the committed MCP examples. It checks that the expected JSON and TOML examples exist, parses them, checks the &lt;code&gt;mcpServers&lt;/code&gt; and &lt;code&gt;mcp_servers&lt;/code&gt; structures, and scans for likely credentials.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;scripts\validate_mcp_configs.py&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OK: sanitized MCP configurations are valid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository also includes three unit tests for the DEV.to article validator. Run them from the checkout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unittest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;discover&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;skills\devto-publish-expert\scripts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test_*.py"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The observed result for the current checkout was three passing tests. These checks do not prove that an external MCP server is reachable, that an account is authenticated, or that a platform's UI will remain unchanged. They prove a narrower and useful property: the examples are structurally valid and do not contain values matching the repository's credential patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser profiles need a different boundary
&lt;/h2&gt;

&lt;p&gt;The Medium and LinkedIn examples use Playwright MCP rather than an API key in the repository. Their local configuration points to a persistent browser profile outside the Git checkout. That profile can contain cookies and authenticated sessions, so it must be treated as private data.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/microsoft/playwright-mcp" rel="noopener noreferrer"&gt;official Playwright MCP documentation&lt;/a&gt; describes persistent, isolated, and extension-based browser modes. It also warns that Playwright MCP is not itself a security boundary. The repository therefore recommends one profile per platform, manual login and MFA, headed browser sessions for publishing workflows, and no committed cookies or storage state.&lt;/p&gt;

&lt;p&gt;If you connect to an existing browser, pay special attention to scope. The official &lt;a href="https://playwright.dev/mcp/configuration/browser-extension" rel="noopener noreferrer"&gt;Playwright browser connection guide&lt;/a&gt; explains that extension mode can reuse logged-in sessions, cookies, and installed extensions. That convenience is exactly why the profile and connection should remain local and supervised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes to expect
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The client cannot start the server
&lt;/h3&gt;

&lt;p&gt;Check the executable and server directory in the local configuration. A placeholder path from the public example is expected to fail. Also check that the server writes protocol messages to stdout and diagnostics to stderr, as the repository README requires for the DEV.to server.&lt;/p&gt;

&lt;h3&gt;
  
  
  The server starts but authentication fails
&lt;/h3&gt;

&lt;p&gt;Confirm the environment variable name and the local secret source. Do not solve this by adding the key to &lt;code&gt;codex.toml&lt;/code&gt;, &lt;code&gt;client.json&lt;/code&gt;, &lt;code&gt;.env.example&lt;/code&gt;, or a Git-tracked file. Rotate a key if it was ever committed or pasted into a shared log.&lt;/p&gt;

&lt;h3&gt;
  
  
  A browser session opens the wrong account
&lt;/h3&gt;

&lt;p&gt;Stop the workflow and inspect the local profile choice. Do not export cookies or storage state to debug it. Use a separate profile per platform and log in manually. A fresh isolated session is safer for testing, but it will not retain the existing login.&lt;/p&gt;

&lt;h3&gt;
  
  
  The validation script reports a possible credential
&lt;/h3&gt;

&lt;p&gt;Treat the warning as a real review item. Inspect the matching file, remove the value from Git history if necessary, rotate the exposed credential, and rerun the validator. A green result is not a substitute for reviewing the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security boundaries
&lt;/h2&gt;

&lt;p&gt;Sanitized examples reduce accidental disclosure; they do not make every MCP server safe. An MCP server may be able to read files, call network services, control a browser, or publish externally. Review the server source, pin versions deliberately, use least-privilege credentials, and test read-only operations before write operations.&lt;/p&gt;

&lt;p&gt;For publishing workflows, the repository's guardrails say to research and validate first, save a draft when possible, show the final destination and content, and stop before the final public action. Approval to open an editor or save a draft is not the same as approval to publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can I commit a real path?
&lt;/h3&gt;

&lt;p&gt;Prefer a placeholder path in a public example. Absolute paths can disclose usernames, private directory layouts, or mounted volumes. Keep machine-specific paths in the local client configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use &lt;code&gt;.env.example&lt;/code&gt; for the real key?
&lt;/h3&gt;

&lt;p&gt;No. It is a variable-name reference. Put the actual value in a local secret manager or client environment that is excluded from Git.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the validator test the remote platform?
&lt;/h3&gt;

&lt;p&gt;No. It validates the repository's local JSON, TOML, and credential patterns. Account access and platform behavior require a separate, supervised smoke test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why keep both JSON and TOML examples?
&lt;/h3&gt;

&lt;p&gt;Different MCP clients accept different configuration formats. Keeping equivalent examples makes the transport shape portable while leaving paths and secrets local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A reusable MCP setup should share intent and structure, not authentication state. Clone the public &lt;a href="https://github.com/paladini/agent-skills" rel="noopener noreferrer"&gt;Agent Skills repository&lt;/a&gt;, inspect its placeholders, copy an example into a user-level configuration, keep profiles and keys outside Git, and run the deterministic validators before connecting a client.&lt;/p&gt;

&lt;p&gt;What is the smallest read-only smoke test you require before allowing an MCP server to perform a write or publish action?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI assistance disclosure: This tutorial was researched and drafted with AI assistance. Repository contents, the current example configuration, validation scripts, and cited primary documentation were checked before publication.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>security</category>
      <category>tutorial</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Turn Moodle Pages into NotebookLM Markdown with a Local Chrome Extension</title>
      <dc:creator>Fernando Paladini</dc:creator>
      <pubDate>Wed, 02 Sep 2026 12:37:26 +0000</pubDate>
      <link>https://dev.to/paladini/turn-moodle-pages-into-notebooklm-markdown-with-a-local-chrome-extension-5e8k</link>
      <guid>https://dev.to/paladini/turn-moodle-pages-into-notebooklm-markdown-with-a-local-chrome-extension-5e8k</guid>
      <description>&lt;p&gt;Moodle content is rarely in one convenient document. A course can spread its material across lessons, pages, files, and embedded videos. Copying each page into a notebook by hand is slow, and a generic scraper can lose the course and module boundaries that make the material useful later.&lt;/p&gt;

&lt;p&gt;This tutorial shows a local workflow using &lt;a href="https://github.com/paladini/extract-moodle-content-to-notebook-lm-extension" rel="noopener noreferrer"&gt;Moodle Content Extractor for NotebookLM&lt;/a&gt;. You will load the open-source Manifest V3 extension, enable capture for a Moodle site, browse the pages you want, and export one Markdown file with course and module structure preserved.&lt;/p&gt;

&lt;p&gt;The project is a browser extension, not a hosted crawler. It processes captured content in the browser and creates files only when you request an export. That boundary matters when course pages contain private or copyrighted material.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will build
&lt;/h2&gt;

&lt;p&gt;The workflow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install the extension from the &lt;a href="https://chromewebstore.google.com/detail/moodle-content-extractor/dcmooninccogclmaimgbmhineffjdjda" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt;, or load the source as an unpacked extension.&lt;/li&gt;
&lt;li&gt;Configure the base URL of your Moodle installation.&lt;/li&gt;
&lt;li&gt;Start capture through the extension popup.&lt;/li&gt;
&lt;li&gt;Browse the lesson and activity pages you want to study.&lt;/li&gt;
&lt;li&gt;Stop capture and export the course as Markdown.&lt;/li&gt;
&lt;li&gt;Upload that Markdown file to NotebookLM yourself if you want a source-grounded study notebook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repository's current stable release is &lt;a href="https://github.com/paladini/extract-moodle-content-to-notebook-lm-extension/releases/tag/v1.4.0" rel="noopener noreferrer"&gt;v1.4.0&lt;/a&gt;. The release notes describe a fix for same-tab Moodle navigation, protection for corrupt or empty module records, and URL normalization for lesson modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome, Edge, or another Chromium-based browser that supports Manifest V3 extensions.&lt;/li&gt;
&lt;li&gt;Access to a Moodle site that you are allowed to read and export.&lt;/li&gt;
&lt;li&gt;A local copy of the extension only if you choose the unpacked installation.&lt;/li&gt;
&lt;li&gt;NotebookLM as an optional destination. The extension does not call NotebookLM or require a NotebookLM account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The release package has no dependency installation step and no build step. If you load from source, use the &lt;code&gt;v1.4.0&lt;/code&gt; tag so the files you test match the stable release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install from source
&lt;/h2&gt;

&lt;p&gt;The Web Store is the shortest path. For a source-based installation, clone the stable tag:&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 &lt;span class="nt"&gt;--branch&lt;/span&gt; v1.4.0 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/paladini/extract-moodle-content-to-notebook-lm-extension.git
&lt;span class="nb"&gt;cd &lt;/span&gt;extract-moodle-content-to-notebook-lm-extension
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;chrome://extensions&lt;/code&gt; in Chromium. Turn on Developer mode, choose Load unpacked, and select the cloned directory. The project is a zero-dependency extension, so there is no &lt;code&gt;npm install&lt;/code&gt; or build command to run.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/paladini/extract-moodle-content-to-notebook-lm-extension/blob/v1.4.0/manifest.json" rel="noopener noreferrer"&gt;manifest&lt;/a&gt; declares Manifest V3, a background service worker, a popup, and permissions for &lt;code&gt;activeTab&lt;/code&gt;, &lt;code&gt;scripting&lt;/code&gt;, &lt;code&gt;storage&lt;/code&gt;, and &lt;code&gt;downloads&lt;/code&gt;. It also declares &lt;code&gt;*://*/*&lt;/code&gt; host access so capture can continue across page navigations after you enable it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture a Moodle course
&lt;/h2&gt;

&lt;p&gt;Click the extension icon and save your Moodle base URL. A value such as &lt;code&gt;https://moodle.example.edu&lt;/code&gt; is a placeholder. Use the origin and path that actually identify your installation, without inventing a URL for a site you do not control.&lt;/p&gt;

&lt;p&gt;Click Start Capture, then browse the Moodle lesson or activity pages you want to include. The extension groups pages using Moodle breadcrumbs and page context. When you are finished, click Stop Capture. You can then export one course or export all captured courses.&lt;/p&gt;

&lt;p&gt;The important operational detail is explicit capture mode. The extension does not treat every page you visit as course content. Its content script checks the configured Moodle base URL and exits on pages that do not match it. This is documented in the project's &lt;a href="https://github.com/paladini/extract-moodle-content-to-notebook-lm-extension/blob/v1.4.0/docs/permissions.html" rel="noopener noreferrer"&gt;permission justification&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An export is structured Markdown rather than a flat text dump. A simplified example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Course Name&lt;/span&gt;

&lt;span class="gu"&gt;## Module: 1.1 Introduction to Databases&lt;/span&gt;
Type: lesson
Captured at: 2026-03-11T02:30:00.000Z

[extracted content]

&lt;span class="gu"&gt;### Videos&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; https://youtube.com/example

&lt;span class="gu"&gt;### Resources&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; https://moodle.example.edu/pluginfile.php/example.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamp and URLs are useful for checking where a passage came from and for keeping exports and backups consistent. The actual export includes the captured page content and any detected video or attachment links that the extractor supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the local design is useful
&lt;/h2&gt;

&lt;p&gt;The extension stores course data in Chrome extension storage. The background service worker handles persistence, while the content script extracts content from a matching Moodle page. The popup provides capture controls, course management, export actions, and backup or restore actions.&lt;/p&gt;

&lt;p&gt;This division keeps the workflow understandable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;content.js&lt;/code&gt; reads the page and extracts course material.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;background.js&lt;/code&gt; groups and stores records in &lt;code&gt;chrome.storage.local&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;popup.js&lt;/code&gt; starts and stops capture and turns stored records into downloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chrome's &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/storage" rel="noopener noreferrer"&gt;Storage API documentation&lt;/a&gt; describes &lt;code&gt;storage.local&lt;/code&gt; as extension-local storage. Chrome documents a default local-storage limit of 10 MB, so a very large course export may need to be split or managed carefully. The extension's local backup and recovery features reduce the chance that a failed write silently destroys the only copy in the browser, but they do not replace an external backup of exported files.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;downloads&lt;/code&gt; permission is used for explicit Markdown and JSON export actions. Chrome's &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/downloads" rel="noopener noreferrer"&gt;downloads API reference&lt;/a&gt; confirms that this permission is required to initiate downloads. The extension does not need a remote server for normal use, and its privacy policy says data leaves the browser when you explicitly export, import a backup, or upload an exported file to another product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the installation before capturing real content
&lt;/h2&gt;

&lt;p&gt;You can perform a low-risk source check before granting access to a Moodle site. Confirm that the manifest reports version &lt;code&gt;1.4.0&lt;/code&gt;, that the background service worker is &lt;code&gt;background.js&lt;/code&gt;, and that the referenced popup and icon files exist in the directory.&lt;/p&gt;

&lt;p&gt;On a machine with Node.js, the JavaScript files can also be parsed without executing them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--check&lt;/span&gt; background.js
node &lt;span class="nt"&gt;--check&lt;/span&gt; content.js
node &lt;span class="nt"&gt;--check&lt;/span&gt; popup.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commands check syntax only. They do not prove that a particular Moodle theme, login flow, embedded video, or attachment format will extract correctly. For a functional check, use a course page you are permitted to process, capture one lesson, export it, and inspect the resulting Markdown before uploading it anywhere.&lt;/p&gt;

&lt;p&gt;Your verification checklist should be concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The course name and module headings are present.&lt;/li&gt;
&lt;li&gt;The captured text matches the page you selected.&lt;/li&gt;
&lt;li&gt;Video and resource links point to the expected locations.&lt;/li&gt;
&lt;li&gt;A page outside the configured Moodle base URL was not added as course content.&lt;/li&gt;
&lt;li&gt;The exported file opens as ordinary Markdown and does not contain unexpected credentials or private browser data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure modes and limits
&lt;/h2&gt;

&lt;p&gt;If a newly visited lesson is missing, confirm that capture mode is still enabled and that the page matches the configured base URL. Same-tab navigation was specifically addressed in the &lt;code&gt;v1.4.0&lt;/code&gt; release, but a Moodle installation can still use custom page structures that the extractor does not recognize.&lt;/p&gt;

&lt;p&gt;If an export is incomplete, inspect the source page and the extension's supported extraction behavior. Embedded content can require its own permissions or may not expose readable text in the page DOM. A linked PDF or presentation is represented as a resource link; the extension does not claim to convert every attachment into Markdown.&lt;/p&gt;

&lt;p&gt;If storage writes fail, reduce the amount captured in one browser storage set and export more frequently. Chrome's storage quota is a real limit, not a promise that an arbitrarily large course can stay in one local record.&lt;/p&gt;

&lt;p&gt;Most importantly, local processing is not the same as unrestricted use. Moodle content may contain student information, licensed textbooks, assessments, or internal course material. Capture only what you are authorized to access, keep exported files private, and review the destination's data-handling terms before uploading them. The extension's &lt;a href="https://github.com/paladini/extract-moodle-content-to-notebook-lm-extension/blob/v1.4.0/docs/privacy.html" rel="noopener noreferrer"&gt;privacy policy&lt;/a&gt; documents its intended local-first behavior, but it cannot control what happens after you export a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does this require an API key?
&lt;/h3&gt;

&lt;p&gt;No. The documented workflow uses the browser extension and local browser storage. NotebookLM is an optional destination, not an API dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it crawl Moodle in the background?
&lt;/h3&gt;

&lt;p&gt;No. You enable capture through the popup and browse the pages yourself. The extension injects its capture logic for matching pages while capture mode is active.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can it export every file attached to a course?
&lt;/h3&gt;

&lt;p&gt;It extracts links to supported resources such as PDF, DOCX, and PPTX. Those links are included in the Markdown; the project does not claim to transform every attachment into text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the current branch identical to the release?
&lt;/h3&gt;

&lt;p&gt;Not necessarily. This tutorial targets the stable &lt;code&gt;v1.4.0&lt;/code&gt; tag. If you use the default branch, inspect its manifest and documentation again because unreleased changes can alter behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A useful NotebookLM source starts before the AI notebook: it starts with a clean, traceable input. This extension turns a user-selected Moodle browsing session into course and module-aware Markdown while keeping capture and export actions visible.&lt;/p&gt;

&lt;p&gt;The practical next step is to test one permitted lesson, inspect the Markdown, and only then decide whether the local export is complete enough for your study workflow.&lt;/p&gt;

&lt;p&gt;What Moodle structure causes the most trouble in your own courses: breadcrumb grouping, embedded media, or attached documents?&lt;/p&gt;

&lt;h2&gt;
  
  
  AI assistance disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance was used to help organize and edit this tutorial. The repository documentation, stable release metadata, Chrome API documentation, source checkout, and syntax checks were reviewed before publication. Claims about the project are limited to those sources and the documented workflow.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>chrome</category>
      <category>tutorial</category>
      <category>notebooklm</category>
    </item>
  </channel>
</rss>
