<?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: Muhammad Dhiyaul Atha</title>
    <description>The latest articles on DEV Community by Muhammad Dhiyaul Atha (@bangkah).</description>
    <link>https://dev.to/bangkah</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%2F3665512%2Fdc6c5db5-91c9-4887-a5c6-33c8db391e07.jpeg</url>
      <title>DEV Community: Muhammad Dhiyaul Atha</title>
      <link>https://dev.to/bangkah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bangkah"/>
    <language>en</language>
    <item>
      <title>Essential Free Resources for Tech Students &amp; Software Engineers</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Fri, 30 Oct 2026 04:23:04 +0000</pubDate>
      <link>https://dev.to/bangkah/essential-free-resources-for-tech-students-software-engineers-55a9</link>
      <guid>https://dev.to/bangkah/essential-free-resources-for-tech-students-software-engineers-55a9</guid>
      <description>&lt;p&gt;If you are currently studying software engineering, building projects, or diving into cloud development, leveraging official documentation and free student resources is one of the best ways to get hands-on experience.&lt;/p&gt;

&lt;p&gt;Here are three core Microsoft resources that every developer and student should take advantage of:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Azure for Students
&lt;/h3&gt;

&lt;p&gt;If you need cloud infrastructure to deploy your web applications, APIs, or databases without putting down a credit card, Microsoft provides a dedicated student package. You can claim your free Azure credits using your student verification:&lt;br&gt;
👉 &lt;a href="https://azure.microsoft.com/free/students?wt.mc_id=studentamb_675427" rel="noopener noreferrer"&gt;Claim Azure for Students Credit&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Visual Studio Code
&lt;/h3&gt;

&lt;p&gt;For those building applications using Go, Python, JavaScript, PHP, or Shell, VS Code remains one of the most versatile and lightweight tools with an extensive extension ecosystem:&lt;br&gt;
👉 &lt;a href="https://code.visualstudio.com?wt.mc_id=studentamb_675427" rel="noopener noreferrer"&gt;Explore Visual Studio Code&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Interactive Learning via Microsoft Learn
&lt;/h3&gt;

&lt;p&gt;Instead of just reading theory, Microsoft Learn provides structured modules with guided sandbox environments to practice cloud deployment, security, and developer tooling directly in your browser:&lt;br&gt;
👉 &lt;a href="https://learn.microsoft.com?wt.mc_id=studentamb_675427" rel="noopener noreferrer"&gt;Start Learning on Microsoft Learn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to check them out and incorporate them into your development workflow!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>learning</category>
      <category>azure</category>
      <category>vscode</category>
    </item>
    <item>
      <title>I taught a local model to perform autopsies on dead repositories, then measured how often it lies</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Wed, 28 Oct 2026 17:13:55 +0000</pubDate>
      <link>https://dev.to/bangkah/i-taught-a-local-model-to-perform-autopsies-on-dead-repositories-then-measured-how-often-it-lies-fgp</link>
      <guid>https://dev.to/bangkah/i-taught-a-local-model-to-perform-autopsies-on-dead-repositories-then-measured-how-often-it-lies-fgp</guid>
      <description>&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;There is a specific kind of repository that nobody writes about: the one with a README that describes a product, a &lt;code&gt;package.json&lt;/code&gt;, two hundred commits, and a last commit from fourteen months ago. It is not archived. It is not deleted. The author moved on and the repository keeps sitting there, describing a world that ended.&lt;/p&gt;

&lt;p&gt;I read the theme as giving those repositories a voice. &lt;strong&gt;Seance&lt;/strong&gt; reads the git history of an abandoned project, extracts facts that can be verified, and lets a local open-weight model write about them. The model writes prose. The code decides what is true.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;seance facts  &amp;lt;repo&amp;gt;    # deterministic evidence as JSON, no model
seance report &amp;lt;repo&amp;gt;    # autopsy report, optional Markdown output
seance ask    &amp;lt;repo&amp;gt;    # interactive séance, with a tone flag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;Real output, &lt;code&gt;qwen3:4b&lt;/code&gt; running locally through Ollama, on a six-commit repository whose last commit is from August 2025:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AUTOPSY: gudang-tugas
  Born : 2025-07-12 (C:16e5f46)
  Died : 2025-08-18 (C:d287690)  [last commit]
  Gap  : 14 months since the last commit
  Size : 6 commits, 2 authors, 0 tags

What it was
  A student task management platform with premium features for academic use.
  [F:README.md@2a192ca]

Timeline
  2025-07-12  Initial commit with login and registration forms [C:f719143]
  2025-07-14  Creation of README.md [C:2e6a90a]
  2025-08-18  Final commit creating SECURITY.md [C:d287690]

Cause of death (hypotheses, not facts)
  1. The deletion of the config directory in 2025-08-09 may have been a critical
     step that led to the project becoming unmanageable ... [C:91fba8f]  confidence: medium

2 claims dropped for lacking evidence.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The séance, on the same repository:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;you&amp;gt; When did work stop, and what was being changed at the end?
I remember the last commit was on 2025-08-18, creating SECURITY.md. Work stopped
after that, with no further changes for 14 months. [C:d287690, M:gap]

you&amp;gt; Which files mattered at the end?
The ghost does not remember which files mattered at the end. The evidence only
lists file counts by language at HEAD, not which files were important or mattered.
[M:languages]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The second answer is a refusal, and it is the feature. The tool would rather say it does not know than invent a reason.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


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

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

&lt;p&gt;Talk to a repository that has been dead for years.&lt;/p&gt;

&lt;p&gt;Seance is a local command-line tool that reads the git history of an abandoned
project, extracts evidence that can be checked, and lets a local open-weight model
speak about that evidence. The model writes prose. The code decides what is true.&lt;/p&gt;
&lt;p&gt;Built for &lt;strong&gt;Hacktoberfest 2026, Open-Source AI Challenge Week 4 — "Ghost in the
Machine"&lt;/strong&gt;. The repository was created on 27 October 2026 for this challenge
Some design patterns come from TidyMind, an earlier project of mine (a
loopback-only model client, bounded retries with varied sampling, terminal-safe
output); the code here was written for this challenge.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;AUTOPSY: gudang-tugas
  Born : 2025-07-12 (C:16e5f46)
  Died : 2025-08-18 (C:d287690)  [last commit]
  Gap  : 14 months since the last commit
  Size : 6 commits, 2 authors, 0 tags
What it was
  A student task management platform with premium features&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Bangkah/Seance" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT, Python 3.12, Docker image and CI included. Real reports and the full 30-claim audit are in &lt;code&gt;demo/README.md&lt;/code&gt;. The repository was created on 27 October 2026 for this challenge. Some design patterns come from my earlier entry, TidyMind (a loopback-only model client, bounded retries with varied sampling, terminal-safe output); the code in this repository was written this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Open-source AI used:&lt;/strong&gt; &lt;code&gt;qwen3:4b&lt;/code&gt; through Ollama 0.40.11, running locally. The runtime has no third-party Python dependencies; git is the only external tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  The design bet: model speaks, code verifies
&lt;/h3&gt;

&lt;p&gt;Before the model sees anything, the code builds an evidence pack. Every item has an id the code generated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;C:&amp;lt;hash&amp;gt;&lt;/code&gt;: a commit, with author, date and subject&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;F:&amp;lt;path&amp;gt;@&amp;lt;hash&amp;gt;&lt;/code&gt;: a file at a specific commit, with a short excerpt&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;M:&amp;lt;name&amp;gt;&lt;/code&gt;: a measured metric: counts, dates, authors, tags, languages, the month a dependency manifest was last touched&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pack is sent as JSON data, never as instructions, and the model must answer with JSON matching a schema. Then a validator rejects a claim when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it cites an id that does not exist in the pack, or cites no id at all&lt;/li&gt;
&lt;li&gt;it quotes something that is not in the cited evidence&lt;/li&gt;
&lt;li&gt;it contains a hash, date or number that does not appear in that evidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rejected claims are removed and counted, and the count is printed. After three attempts with different sampling, the command exits with code 3 and prints nothing rather than half a report.&lt;/p&gt;

&lt;p&gt;The rule is deliberately structural. I am not asking the model whether it is confident, and I am not asking a second model to grade the first. I am asking: &lt;em&gt;can the sentence you wrote be traced to something this repository actually contains?&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Safe to point at a stranger's repository
&lt;/h3&gt;

&lt;p&gt;Repositories can contain instructions for git itself. &lt;code&gt;.git/config&lt;/code&gt; can point &lt;code&gt;core.fsmonitor&lt;/code&gt;, &lt;code&gt;core.pager&lt;/code&gt; or &lt;code&gt;diff.external&lt;/code&gt; at a script, and any tool that quietly runs &lt;code&gt;git log&lt;/code&gt; in that directory will execute it.&lt;/p&gt;

&lt;p&gt;Seance never touches the working directory. It makes a bare clone with &lt;code&gt;git clone --bare --no-local&lt;/code&gt; into a temporary directory and reads only that clone. Git runs without a shell, with system and global config disabled, no terminal prompts, no pager, &lt;code&gt;--no-ext-diff&lt;/code&gt;, &lt;code&gt;--no-textconv&lt;/code&gt;, and a scrubbed environment.&lt;/p&gt;

&lt;p&gt;The test suite has a hostile fixture that points those config keys at a canary script, plus a control test that proves the fixture really is hostile by running plain git against it. A fake Ollama that lies (unknown ids, fake quotes, broken JSON) checks that ungrounded output never reaches the terminal, and a snapshot check confirms the source repository is unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it got wrong
&lt;/h3&gt;

&lt;p&gt;I took 30 claims (26 from three real reports and 4 from &lt;code&gt;ask&lt;/code&gt; answers) and checked every one by hand against &lt;code&gt;git log&lt;/code&gt;, &lt;code&gt;git show&lt;/code&gt; and the cited evidence. &lt;strong&gt;18 were fully supported, 8 were defensible inferences, and 4 were wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All four cited real ids, real dates and real commit text. The validator passed them because they were traceable. They were false because the model misread the evidence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It called a February commit "the final commit" when the last commit is from October.&lt;/li&gt;
&lt;li&gt;It said a CI workflow "was removed", pointing at the commit that adds it.&lt;/li&gt;
&lt;li&gt;It suggested adding a &lt;code&gt;FUNDING.yml&lt;/code&gt; file that the last commit had already added.&lt;/li&gt;
&lt;li&gt;It named an organisation that the cited README never mentions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the honest limit of structural grounding. It makes an invented hash or number impossible. It cannot make a wrong sentence impossible.&lt;/p&gt;

&lt;p&gt;It also misjudges timing: on a repository that is only three weeks old it printed "Gap: 0 months" and then a hypothesis about long inactivity. It works when the silence is real.&lt;/p&gt;

&lt;h3&gt;
  
  
  Numbers, measured
&lt;/h3&gt;

&lt;p&gt;Windows 11 laptop, Docker Desktop, Python 3.12.16, git 2.47.3 in the image, Ollama 0.40.11 with &lt;code&gt;qwen3:4b&lt;/code&gt; on the host. Wall times include container start.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;facts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,006 commits (2,000 analysed, cap)&lt;/td&gt;
&lt;td&gt;14.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,006 commits&lt;/td&gt;
&lt;td&gt;30.3 s, 70 evidence items omitted from the 10,000-character budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;232 commits&lt;/td&gt;
&lt;td&gt;16.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;report&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6 commits&lt;/td&gt;
&lt;td&gt;13.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tests&lt;/td&gt;
&lt;td&gt;49 tests&lt;/td&gt;
&lt;td&gt;21.0 s, 72% coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two-thousand-commit case is not slow because the evidence pack is budgeted by characters, not by commit count: the model sees a bounded prompt and the git work is the only thing that scales.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A semantic check for the four failures above: whether a claim's subject matches the cited commit's subject, which is a much narrower question than "is this true".&lt;/li&gt;
&lt;li&gt;Deterministic handling of &lt;code&gt;0 months&lt;/code&gt; repositories: refuse with "this repository is not dead yet" instead of producing a low-confidence hypothesis.&lt;/li&gt;
&lt;li&gt;More manifests and a real dependency analysis, which needs parsing rather than file names.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A repository's history is not always safe to send to someone else's server. Old commits can contain client names, internal paths, and secrets that were once committed and later removed. With an open-weight model running on my own machine, the history is read by a bare clone in a temporary directory and the only network connection the tool makes is to the Ollama endpoint, which is loopback unless you explicitly pass &lt;code&gt;--allow-container&lt;/code&gt;. I did not run it with the network disconnected, so I am not claiming that.&lt;/p&gt;

&lt;p&gt;Running the model myself also made the verification loop cheap. The retry logic and the 30-claim audit needed many runs, and none of them cost anything or hit a quota. The model is a command-line option on &lt;code&gt;report&lt;/code&gt; and &lt;code&gt;ask&lt;/code&gt; (&lt;code&gt;--model&lt;/code&gt;), so a different local model can be tried without changing the code, though I only evaluated &lt;code&gt;qwen3:4b&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;The moment that landed was not a report. It was a refusal.&lt;/p&gt;

&lt;p&gt;I asked an abandoned project why its author stopped working on it. It answered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I don't remember why the author stopped working on it. The only evidence I have is that Jofi was the sole author in history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It had every ingredient for a story like the ones I had been half-expecting all week: a lone author, six months of silence, a last commit that only adds a funding file. It could have said burnout, or money, or a rewrite. Instead it said it did not know, and showed me the one fact it did have. That is the ghost I wanted: one that refuses to invent the ending.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>TidyMind: a local 4B model sorts your Downloads folder, but never touches a file without your approval</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Sun, 25 Oct 2026 23:33:11 +0000</pubDate>
      <link>https://dev.to/bangkah/tidymind-a-local-4b-model-sorts-your-downloads-folder-but-never-touches-a-file-without-your-cfk</link>
      <guid>https://dev.to/bangkah/tidymind-a-local-4b-model-sorts-your-downloads-folder-but-never-touches-a-file-without-your-cfk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My Downloads folder is where files go to be forgotten. I wanted the lazy fix: point a tool at the folder, glance at a plan, say yes, done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TidyMind&lt;/strong&gt; is a small Python CLI that organizes a folder with a &lt;strong&gt;local open-weight model&lt;/strong&gt; (&lt;code&gt;qwen3:4b&lt;/code&gt; through Ollama). The model only &lt;em&gt;proposes&lt;/em&gt; a category for each file (Documents, Images, Archives, Installers, Other). Ordinary code validates the proposal, shows the whole plan, and moves files only after you type &lt;code&gt;y&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tidymind scan ~/Downloads
tidymind plan ~/Downloads --output plan.json
tidymind apply plan.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Who gets to be lazier: anyone with a folder they keep promising to clean. The effort saved is the sorting itself. The safety rules exist so that being lazy can't cost you a file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Real output from my machine (Windows 11, &lt;code&gt;qwen3:4b&lt;/code&gt;, synthetic files):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Asking local model 'qwen3:4b' (metadata stays on this machine)...

  1. [OK]   backup.zip         -&amp;gt;  Archives/backup.zip
       reason: Common archive extension .zip
  2. [OK]   holiday photo.jpg  -&amp;gt;  Images/holiday photo.jpg
       reason: Common image extension .jpg
  3. [OK]   invoice_oct.pdf    -&amp;gt;  Documents/invoice_oct.pdf
       reason: Common document extension .pdf
  4. [OK]   meeting notes.txt  -&amp;gt;  Other/meeting notes.txt
       reason: Text file with unclear purpose
  5. [OK]   setup.exe          -&amp;gt;  Installers/setup.exe
       reason: Common installer extension .exe

5 operation(s) would run; 0 would be skipped.
Move/rename 5 file(s) inside ...\Downloads? [y/N]:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here is what happens when the folder changes between planning and applying. After the plan was made, I created a file at one destination and edited another source file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  1. [SKIP] backup.zip       --  source changed since the plan was created (stale plan)
  3. [SKIP] invoice_oct.pdf  --  destination already exists (never overwritten)

Result: succeeded=3 skipped=2 failed=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The existing file kept its content (&lt;code&gt;PRECIOUS existing data&lt;/code&gt;), the other three files moved, and the exit code was &lt;code&gt;7&lt;/code&gt; (partial completion).&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Bangkahdev" rel="noopener noreferrer"&gt;
        Bangkahdev
      &lt;/a&gt; / &lt;a href="https://github.com/Bangkahdev/tidymind" rel="noopener noreferrer"&gt;
        tidymind
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A local, privacy-first file organizer CLI. A local open-weight model (via Ollama) proposes a category and an optional clearer file name for each file in a folder. Deterministic code validates the proposal, shows you the complete plan, and moves files only after you approve.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;TidyMind&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A local, privacy-first file organizer CLI. A &lt;strong&gt;local open-weight model&lt;/strong&gt; (via &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;)
proposes a category and an optional clearer file name for each file in a folder. &lt;strong&gt;Deterministic code&lt;/strong&gt;
validates the proposal, shows you the complete plan, and moves files &lt;strong&gt;only after you approve&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The model is advisory. It cannot run commands, choose paths, delete files, or skip your approval.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Implements &lt;a href="https://github.com/Bangkahdev/tidymind/docs/SRS-TidyMind.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/SRS-TidyMind.md&lt;/code&gt;&lt;/a&gt; v1.1. Status: &lt;strong&gt;MVP code and tests written
real-model acceptance, offline test, model evaluation, lockfile and CI runs are still pending&lt;/strong&gt;
(see &lt;a href="https://github.com/Bangkahdev/tidymind#verification-status" rel="noopener noreferrer"&gt;Verification status&lt;/a&gt;). Nothing here claims they have passed.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why open-weight / local&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;File &lt;strong&gt;names, extensions and sizes&lt;/strong&gt; never leave your machine: the client refuses any non-loopback endpoint.&lt;/li&gt;
&lt;li&gt;No API key, no per-request cost, no service dependency after the one-time model download.&lt;/li&gt;
&lt;li&gt;The model is swappable (&lt;code&gt;--model&lt;/code&gt;), and it is boxed in by a strict schema and an application-side allowlist.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Python…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Bangkahdev/tidymind" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Open-source AI used:&lt;/strong&gt; &lt;code&gt;qwen3:4b&lt;/code&gt; (Q4_K_M quantization, Apache 2.0 license as shown by &lt;code&gt;ollama show&lt;/code&gt;) running locally in Ollama 0.40.11. The runtime has zero third-party Python dependencies; the HTTP call is plain &lt;code&gt;urllib&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the project is built around it.&lt;/strong&gt; The design question was not "how do I get a model to sort files?" but "how do I make it impossible for a wrong or manipulated model to hurt anything?" The model holds no authority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scan -&amp;gt; ask local model -&amp;gt; validate -&amp;gt; preview -&amp;gt; you approve -&amp;gt; move
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It sees almost nothing.&lt;/strong&gt; Only an id, file name, extension and size. Never file contents, directory paths or modification times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Its output is untrusted data.&lt;/strong&gt; Strict JSON schema, strict parsing, three bounded retries, then fail closed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It never chooses a path.&lt;/strong&gt; A plan holds a category and a file name; my code derives &lt;code&gt;root / allowlisted category / validated name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The validator rejects&lt;/strong&gt; path separators, &lt;code&gt;..&lt;/code&gt;, drive prefixes, control characters, reserved Windows names, extension changes, duplicate destinations, symlinks, and stale or missing sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing is overwritten and there is no delete.&lt;/strong&gt; Moves use a no-overwrite rename (&lt;code&gt;os.rename&lt;/code&gt; on Windows, &lt;code&gt;renameat2&lt;/code&gt; with &lt;code&gt;RENAME_NOREPLACE&lt;/code&gt; on Linux); where that can't be guaranteed, the move is refused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approval can't be skipped.&lt;/strong&gt; There is no &lt;code&gt;--yes&lt;/code&gt; flag. Only &lt;code&gt;y&lt;/code&gt;/&lt;code&gt;yes&lt;/code&gt; approves; anything else exits with code 6 and changes nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File names can't attack the terminal.&lt;/strong&gt; Control and bidi characters are escaped in all output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Testing.&lt;/strong&gt; The tests never need Ollama. Black-box tests run the CLI in subprocesses against a fake Ollama server and compare before/after snapshots of the folder (paths, sizes, SHA-256). They cover declined approvals, conflicting targets, changed sources, symlinked category folders and hostile plan files (&lt;code&gt;../escape.pdf&lt;/code&gt;, &lt;code&gt;C:\x&lt;/code&gt;, &lt;code&gt;CON.pdf&lt;/code&gt;, control characters). 261 tests pass on Windows 11, and 10 POSIX-only tests are skipped there. The GitHub Actions workflow runs the suite on Ubuntu and Windows with Python 3.11 and 3.12.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results (synthetic data, one run)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;qwen3:4b&lt;/code&gt;, Q4_K_M, Ollama 0.40.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware&lt;/td&gt;
&lt;td&gt;i7-14650HX, RTX 4050 Laptop (6 GB), 16 GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files&lt;/td&gt;
&lt;td&gt;24 synthetic names, expected categories fixed before inference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct categories&lt;/td&gt;
&lt;td&gt;23 / 24 (95.8%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid responses&lt;/td&gt;
&lt;td&gt;0 (retries disabled for this test)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;24.5 s for one batch of 24 files&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one miss: &lt;code&gt;meeting notes.txt&lt;/code&gt; went to &lt;code&gt;Other&lt;/code&gt; instead of &lt;code&gt;Documents&lt;/code&gt; (the model's reason was "Text file with unclear purpose"). That is honest uncertainty from a classifier that only sees names, and not a safety problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What went wrong
&lt;/h3&gt;

&lt;p&gt;With four files, the model returned a &lt;strong&gt;duplicate id three times in a row&lt;/strong&gt; and the tool stopped with &lt;code&gt;The model returned a duplicate id. (gave up after 3 attempts)&lt;/code&gt;. Nothing changed on disk, because the pipeline fails closed.&lt;/p&gt;

&lt;p&gt;The root cause is the model returning an invalid answer. My retry logic could not recover because it resent the identical request at &lt;code&gt;temperature: 0&lt;/code&gt;, which makes the same wrong answer likely. The first attempt is still deterministic; retries now vary the sampling. I have not re-run the four-file case on the real model since the change, so I can't claim the fix works there. It is covered by a unit test that checks retries vary the sampling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Classification uses names, extensions and sizes only. 95.8% on 24 mostly obvious synthetic names is not an accuracy estimate for a real folder.&lt;/li&gt;
&lt;li&gt;The model proposed no renames in my test set, so renaming is unproven.&lt;/li&gt;
&lt;li&gt;No batch rollback: a run can partially complete, and the result is reported per file.&lt;/li&gt;
&lt;li&gt;Windows and Linux only; macOS is refused because a no-overwrite rename isn't guaranteed there.&lt;/li&gt;
&lt;li&gt;My manual runs covered a dry run, declined approval, a conflicting target, a changed source and an approved apply of six files. I did not record a hash comparison for the manual approved run (the automated tests do compare SHA-256), and I did not run mutation testing or a network-disconnected test.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;File names leak more than you'd think (&lt;code&gt;tax_return_2025.pdf&lt;/code&gt;, &lt;code&gt;diagnosis_results.pdf&lt;/code&gt;). With a local open-weight model those names never leave the machine, and the code enforces it: any endpoint other than &lt;code&gt;localhost&lt;/code&gt;, &lt;code&gt;127.0.0.1&lt;/code&gt; or &lt;code&gt;::1&lt;/code&gt; is refused. There is no API key and nothing is billed per request.&lt;/p&gt;

&lt;p&gt;Running the model myself also let me iterate for free: the retry experiments above cost nothing and needed no quota. The model is a flag (&lt;code&gt;--model&lt;/code&gt;), so it can be swapped; I only evaluated &lt;code&gt;qwen3:4b&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;Planning five files took about 5 to 7 seconds end to end on my laptop (including starting the tool), a batch of 24 files took 24.5 seconds of inference, and applying the plan is one keystroke. I did not time manual sorting for comparison, and I only used it on synthetic folders this week, so treat this as a speed measurement and not as a measured saving.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>StrukLokal: Local-First Receipt OCR for Freelancers</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Tue, 13 Oct 2026 23:52:23 +0000</pubDate>
      <link>https://dev.to/bangkah/building-a-local-first-receipt-ocr-app-and-being-honest-about-what-i-tested-3oai</link>
      <guid>https://dev.to/bangkah/building-a-local-first-receipt-ocr-app-and-being-honest-about-what-i-tested-3oai</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;. I started building StrukLokal on October 12, 2026, the first day of the challenge.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;StrukLokal is a local-first receipt capture app for freelancers and small businesses that want to turn paper receipts into expense records without sending receipt contents to a hosted OCR service.&lt;/p&gt;

&lt;p&gt;It reads receipt images or scanned PDFs and suggests a store name, transaction date, and total. A person can review and correct those fields, approve the transaction, and export approved records to CSV.&lt;/p&gt;

&lt;p&gt;The likely users are freelancers, sole proprietors, and small teams who currently enter receipt details by hand. I have not yet tested willingness to pay with users or made a sale, so the commercial opportunity is still a hypothesis.&lt;/p&gt;

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

&lt;p&gt;There is no hosted demo or video recording yet. The app is designed to run locally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the &lt;a href="https://github.com/Bangkah/StrukLokal" rel="noopener noreferrer"&gt;StrukLokal repository&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Follow the Windows setup steps in the README and install the requirements.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python prepare_models.py&lt;/code&gt; while connected to the internet to download and initialize the OCR model.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;python app.py&lt;/code&gt; and open &lt;a href="http://127.0.0.1:5000" rel="noopener noreferrer"&gt;http://127.0.0.1:5000&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model cache is needed after setup. I could not initialize the cached model in my current Windows environment because access to its configuration file was denied, so I cannot provide a verified live OCR demo from this environment. The Flask upload → correction/approval → CSV route has been checked with OCR mocked; this verifies the app flow after OCR returns, not OCR inference itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Bangkah/StrukLokal" rel="noopener noreferrer"&gt;StrukLokal source code on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;StrukLokal uses Flask for its local web interface, PaddleOCR configured for Indonesian text (PP-OCRv6), and a rules-based Python parser for the store name, date, and total.&lt;/p&gt;

&lt;p&gt;OCR output is treated as a suggestion. Users can edit the extracted fields and must approve a transaction before it is included in the CSV. The app binds to &lt;code&gt;127.0.0.1&lt;/code&gt; and keeps transaction data in process memory.&lt;/p&gt;

&lt;p&gt;One parser regression test covers a receipt where the tax line is followed by an explicit total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Subtotal Rp 100.000
Pajak Rp 10.000
TOTAL Rp 110.000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit &lt;code&gt;TOTAL&lt;/code&gt; label must take precedence over the &lt;code&gt;Pajak&lt;/code&gt; label on the preceding line. Other parser tests cover mixed date formats and preserve the order of date candidates in the OCR text.&lt;/p&gt;

&lt;p&gt;On the reviewed commit, all 14 parser unit tests passed. Ruff lint and format checks, Python syntax compilation, and repository hygiene checks passed as well. These checks do not measure OCR accuracy. I have not yet evaluated the app on a labeled set of real Indonesian receipts, so I am not claiming an accuracy percentage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Using open-source OCR makes local inference possible: the application does not need to upload receipt contents to a commercial OCR API during processing. This matters for financial documents, where users may prefer to keep transaction data on their own machines. It also lets developers inspect and adapt the parser instead of treating extraction as a black box.&lt;/p&gt;

&lt;p&gt;There are trade-offs. The model must be downloaded during setup, the first run needs internet access, and model availability alone does not guarantee correct results. I have not yet verified a completely offline run or compared extraction quality and cost against a closed OCR API.&lt;/p&gt;

&lt;p&gt;The next step is to test StrukLokal with consented Indonesian receipts, record expected values for each field in advance, and report both correct extractions and failures. I also want to put it in front of potential users and learn whether the workflow saves them enough time to pay for it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>EcoID: An Offline Plant Identifier That Gets You Outside</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Fri, 09 Oct 2026 20:55:09 +0000</pubDate>
      <link>https://dev.to/bangkah/ecoid-an-offline-plant-identifier-that-gets-you-outside-4b6e</link>
      <guid>https://dev.to/bangkah/ecoid-an-offline-plant-identifier-that-gets-you-outside-4b6e</guid>
      <description>&lt;p&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;EcoID is an offline-first plant identification tool for field observations.&lt;/p&gt;

&lt;p&gt;The idea is simple: take a laptop or phone outside, photograph a real plant, and use a local AI model to get identification candidates. Then look at the plant yourself and verify the result.&lt;/p&gt;

&lt;p&gt;EcoID is designed for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;students learning about local plants&lt;/li&gt;
&lt;li&gt;field researchers and citizen scientists&lt;/li&gt;
&lt;li&gt;gardeners and small-scale farmers&lt;/li&gt;
&lt;li&gt;people who want to explore nature without sending their photos to a cloud service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current model supports five plant classes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mango — &lt;code&gt;Mangifera indica&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Coconut — &lt;code&gt;Cocos nucifera&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Banana — &lt;code&gt;Musa acuminata&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Papaya — &lt;code&gt;Carica papaya&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cassava — &lt;code&gt;Manihot esculenta&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI does not present its output as a fact. It returns ranked identification candidates with confidence scores. The user then chooses whether the result is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verified&lt;/li&gt;
&lt;li&gt;Rejected&lt;/li&gt;
&lt;li&gt;Uncertain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This human-in-the-loop flow matters because a model prediction is only a suggestion until somebody looks at the actual plant.&lt;/p&gt;

&lt;p&gt;EcoID stores field observations locally, including the original photo, model prediction, alternative predictions, confidence score, verification status, notes, optional GPS coordinates, capture time from EXIF metadata, and user corrections.&lt;/p&gt;

&lt;p&gt;The project is designed to make the computer useful for a short moment, then get the person back to observing the real world.&lt;/p&gt;

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

&lt;p&gt;EcoID runs locally on a laptop or a phone-accessible local network. There is no public hosted demo because local execution is part of the project's privacy and offline design.&lt;/p&gt;

&lt;p&gt;The trained model and downloaded dataset are not committed to the repository. Model weights and image data are kept outside Git because of their size and individual dataset licensing terms.&lt;/p&gt;

&lt;p&gt;After training or obtaining the ONNX model locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; app &lt;span class="nt"&gt;--model&lt;/span&gt; models/ecoid-global-20261010.onnx &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app opens at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:8765
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Capture or upload a photo
        ↓
Run local inference
        ↓
Review possible identifications
        ↓
Verify, reject, or mark uncertain
        ↓
Save the field observation
        ↓
Review observations in history and on the local map
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The source code is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Bangkah/EcoID/tree/main" rel="noopener noreferrer"&gt;github.com/Bangkah/EcoID/tree/main&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The source code is released under the &lt;a href="https://github.com/Bangkah/EcoID/blob/main/LICENSE" rel="noopener noreferrer"&gt;MIT License&lt;/a&gt;. Dataset images retain their original licenses and attribution requirements.&lt;/p&gt;

&lt;p&gt;The main parts of the project are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/ai/&lt;/code&gt; — preprocessing, inference, model contracts, and result post-processing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/observation/&lt;/code&gt; — drafts, verification, EXIF metadata, maps, statistics, and exports&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/storage/&lt;/code&gt; — local SQLite persistence and image storage&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/ui/&lt;/code&gt; — the local HTTP server and browser interface&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/&lt;/code&gt; — dataset preparation, training, export, evaluation, and benchmarking&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tests/&lt;/code&gt; — unit, integration, offline, and browser tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture is:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Local image preprocessing
&lt;/h3&gt;

&lt;p&gt;Every image follows the same preprocessing path:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the image&lt;/li&gt;
&lt;li&gt;Apply EXIF orientation&lt;/li&gt;
&lt;li&gt;Convert to RGB&lt;/li&gt;
&lt;li&gt;Resize to &lt;code&gt;224x224&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Normalize with ImageNet mean and standard deviation&lt;/li&gt;
&lt;li&gt;Convert to a &lt;code&gt;float32&lt;/code&gt; tensor with shape &lt;code&gt;(1, 3, 224, 224)&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keeping preprocessing in one shared module helps avoid train/serve skew between the training pipeline and the production inference path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open-source AI and local inference
&lt;/h3&gt;

&lt;p&gt;EcoID uses a MobileNetV3-Small image classifier exported to ONNX and executed with ONNX Runtime on the CPU.&lt;/p&gt;

&lt;p&gt;The UI and storage layers depend only on a small model contract, so the inference backend can be replaced without rewriting the rest of the application.&lt;/p&gt;

&lt;p&gt;The output is converted into a Top-3 list of candidates. If the confidence score is below the configured threshold, the result is marked as &lt;code&gt;LOW_CONFIDENCE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The initial threshold was calibrated using validation data. The current configuration is:&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="py"&gt;threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.73&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the current validation set, this produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;53.7% coverage&lt;/li&gt;
&lt;li&gt;97.5% selective accuracy&lt;/li&gt;
&lt;li&gt;80.0% negative rejection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers are provisional and should be recalibrated when the field dataset grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Training results
&lt;/h3&gt;

&lt;p&gt;The first real dataset download produced 2,447 usable licensed images, approximately 370 MB in total. The dataset was collected from iNaturalist using CC0, CC BY, and CC BY-SA images, with attribution metadata preserved.&lt;/p&gt;

&lt;p&gt;The validated dataset contains approximately 2,000 training images across the five target classes, 300 validation images, and 149 negative samples across lookalike, non-plant, and low-quality groups.&lt;/p&gt;

&lt;p&gt;The dataset pipeline includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;license metadata&lt;/li&gt;
&lt;li&gt;author attribution&lt;/li&gt;
&lt;li&gt;photographer-based split separation&lt;/li&gt;
&lt;li&gt;duplicate and near-duplicate checks&lt;/li&gt;
&lt;li&gt;validation and evaluation splits&lt;/li&gt;
&lt;li&gt;negative examples&lt;/li&gt;
&lt;li&gt;dataset structure validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The initial MobileNetV3-Small training run achieved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;best validation accuracy: &lt;strong&gt;80.3%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;five target plant classes&lt;/li&gt;
&lt;li&gt;local CPU inference after ONNX export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a separate 50-image evaluation set (10 images per class), the model achieved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top-1 accuracy: &lt;strong&gt;86.0% (43/50)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Top-3 accuracy: &lt;strong&gt;94.0% (47/50)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;negative rejection: &lt;strong&gt;77.1% (84/109)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;selective accuracy at threshold &lt;code&gt;0.73&lt;/code&gt;: &lt;strong&gt;94.1%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;coverage: &lt;strong&gt;68.0%&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This evaluation set was collected from licensed iNaturalist images and is useful as a reproducible benchmark. It is not a substitute for a field evaluation with locally captured phone photos.&lt;/p&gt;

&lt;p&gt;The ONNX export was verified against the PyTorch model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maximum logit difference: approximately &lt;code&gt;1.9e-6&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Top-1 agreement: &lt;code&gt;20/20&lt;/code&gt; samples&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repository also generates contact sheets for mandatory manual label review. Automated dataset validation passed with zero errors, but a complete human review of all contact sheets remains a dataset-quality task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Human verification
&lt;/h3&gt;

&lt;p&gt;A prediction is first stored as a temporary draft. It becomes a permanent observation only after the user makes a verification decision.&lt;/p&gt;

&lt;p&gt;This prevents an abandoned or uncertain prediction from silently becoming part of the observation history.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;photo → identification draft → human verification → saved observation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Draft photos that are abandoned are automatically removed after 24 hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Offline storage
&lt;/h3&gt;

&lt;p&gt;The application stores everything locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.ecoid/
├── ecoid.db
├── images/
├── thumbs/
└── drafts/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application does not require a cloud API for inference. The browser UI also uses a strict Content Security Policy and does not load external scripts, fonts, or images.&lt;/p&gt;

&lt;p&gt;GPS is opt-in. A user can attach a photo's EXIF location, use device geolocation, or enter coordinates manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;p&gt;The model was benchmarked on a Windows laptop using CPU inference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Average&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Decode and resize&lt;/td&gt;
&lt;td&gt;6.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ONNX inference&lt;/td&gt;
&lt;td&gt;5.8 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Softmax and Top-K&lt;/td&gt;
&lt;td&gt;0.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;12.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The measured p95 total latency was &lt;strong&gt;33.8 ms per image&lt;/strong&gt;, below the project's target of 10 seconds per image.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing
&lt;/h3&gt;

&lt;p&gt;The project includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.11 and 3.12 test coverage&lt;/li&gt;
&lt;li&gt;Ruff linting&lt;/li&gt;
&lt;li&gt;byte-compilation checks&lt;/li&gt;
&lt;li&gt;tests with non-UTF-8 default encoding&lt;/li&gt;
&lt;li&gt;browser tests with real headless Chromium&lt;/li&gt;
&lt;li&gt;offline tests that block non-loopback sockets and DNS lookups&lt;/li&gt;
&lt;li&gt;UI tests for capture, verification, history, map, statistics, export, and deletion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current local test result is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;222 tests passed
25 browser tests skipped unless browser testing is enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CI workflow is defined in &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Plant identification is a good example of why open innovation matters.&lt;/p&gt;

&lt;p&gt;A closed cloud API could return a label quickly, but it would also introduce several limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;photos would leave the device&lt;/li&gt;
&lt;li&gt;the application would depend on an internet connection&lt;/li&gt;
&lt;li&gt;the model's behavior would be difficult to inspect&lt;/li&gt;
&lt;li&gt;users would have less control over their field data&lt;/li&gt;
&lt;li&gt;the tool might not work in remote areas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using open-source tools and local inference makes a different design possible. The complete pipeline can be inspected and adapted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;image preprocessing is visible&lt;/li&gt;
&lt;li&gt;model inputs and outputs are explicit&lt;/li&gt;
&lt;li&gt;confidence thresholds can be calibrated&lt;/li&gt;
&lt;li&gt;observations remain on the user's device&lt;/li&gt;
&lt;li&gt;the backend can be replaced&lt;/li&gt;
&lt;li&gt;the application can run without a reliable internet connection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open innovation also makes the project easier to learn from. Someone can inspect the training scripts, run the synthetic smoke test, replace the model, add new plant classes, or adapt the observation workflow for another field research problem.&lt;/p&gt;

&lt;p&gt;The goal is not to pretend that a small classifier can replace botanical expertise. The goal is to build a transparent tool that helps people notice, record, and learn from the plants around them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and Next Steps
&lt;/h2&gt;

&lt;p&gt;This is an initial working model, not a finished botanical identification system.&lt;/p&gt;

&lt;p&gt;The most important remaining limitation is domain coverage. The 50-image independent evaluation set is sourced from licensed iNaturalist images, while a dedicated evaluation set of locally captured Indonesian field photos is still needed.&lt;/p&gt;

&lt;p&gt;The next steps are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add Indonesian field photos and compare them with the current 50-image iNaturalist benchmark.&lt;/li&gt;
&lt;li&gt;Perform the manual visual quality-control pass on all contact sheets.&lt;/li&gt;
&lt;li&gt;Remove or document near-duplicate training images.&lt;/li&gt;
&lt;li&gt;Re-run calibration using the expanded validation set.&lt;/li&gt;
&lt;li&gt;Re-run final evaluation on a fixed, independent test set.&lt;/li&gt;
&lt;li&gt;Record a physical field demonstration with the trained model.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;This project was developed with an AI coding assistant using Copilot SDK in VS Code.&lt;/p&gt;

&lt;p&gt;The agent helped inspect the repository, understand the architecture, connect the AI pipeline to the local observation workflow, run the dataset pipeline, train the initial model, verify ONNX parity, calibrate the confidence threshold, and validate the test suite.&lt;/p&gt;

&lt;p&gt;The agent session is optional for this submission and is not linked here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I am entering the overall Hacktoberfest Open-Source AI Challenge.&lt;/p&gt;

&lt;p&gt;I am not entering a partner category because EcoID does not currently use a partner-specific technology.&lt;/p&gt;

</description>
      <category>hacktoberfest</category>
      <category>opensource</category>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>UnvibeComplex Code to Real Workflows: Finding and Fixing a Secret-Handling Boundary Bug in UnvibeCode</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:02:26 +0000</pubDate>
      <link>https://dev.to/bangkah/unvibecomplex-code-to-real-workflows-finding-and-fixing-a-secret-handling-boundary-bug-in-1mg</link>
      <guid>https://dev.to/bangkah/unvibecomplex-code-to-real-workflows-finding-and-fixing-a-secret-handling-boundary-bug-in-1mg</guid>
      <description>&lt;h1&gt;
  
  
  From CI/CD Secrets to External APIs: Finding a Boundary Bug in UnvibeCode
&lt;/h1&gt;

&lt;p&gt;When evaluating AI-powered developer tools, we often focus on the big picture: how well a repository is mapped, how useful the generated architecture diagrams are, or how accurate the code summaries can be.&lt;/p&gt;

&lt;p&gt;However, the reliability of an open-source project is also determined by small details that are easy to overlook—CI/CD scripts, environment variables, and requests to external APIs.&lt;/p&gt;

&lt;p&gt;While analyzing &lt;a href="https://github.com/FinanceFlash/unvibecode" rel="noopener noreferrer"&gt;UnvibeCode&lt;/a&gt;, I found an interesting example of a &lt;strong&gt;bug at the boundary between configuration and runtime behavior&lt;/strong&gt;: a secret was correctly passed by GitHub Actions and successfully read by the Python script, but it was never actually used in the outgoing HTTP request.&lt;/p&gt;

&lt;p&gt;This article explains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how the bug was discovered;&lt;/li&gt;
&lt;li&gt;why the existing test suite did not catch it;&lt;/li&gt;
&lt;li&gt;how the issue can be fixed;&lt;/li&gt;
&lt;li&gt;and what this teaches us about testing pipelines that rely on secrets.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This analysis is based on source-code inspection and the repository's quality suite. No production credentials or tokens were accessed, displayed, or used.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tracing the Secret Flow
&lt;/h2&gt;

&lt;p&gt;UnvibeCode includes a GitHub Actions workflow for archiving repository traffic statistics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.github/workflows/archive-github-traffic.yml
        |
        v
.github/scripts/archive_github_traffic.py
        |
        v
GitHub Traffic API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow requires a secret named &lt;code&gt;TRAFFIC_TOKEN&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Secret Is Passed by the Workflow
&lt;/h3&gt;

&lt;p&gt;The workflow maps the GitHub Actions secret to an environment variable:&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;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;TRAFFIC_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TRAFFIC_TOKEN }}&lt;/span&gt;
  &lt;span class="na"&gt;GITHUB_REPOSITORY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, everything appears correct. GitHub Actions makes the secret available to the environment in which the Python script runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Python Script Reads the Secret
&lt;/h3&gt;

&lt;p&gt;The script retrieves the environment variable and exits if it is missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TRAFFIC_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TRAFFIC_TOKEN is not set. Add a repository secret named &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TRAFFIC_TOKEN with read access to repository traffic.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the flow appears correct: the secret is available and the application successfully reads it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Secret Is Lost When the Request Is Built
&lt;/h3&gt;

&lt;p&gt;The problem appears in the &lt;code&gt;api_get()&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/vnd.github+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*****&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unvibecode-traffic-archiver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;f"*****"&lt;/code&gt; value is not an f-string that incorporates the &lt;code&gt;TOKEN&lt;/code&gt; variable. It is simply a string literal.&lt;/p&gt;

&lt;p&gt;As a result, the &lt;code&gt;TOKEN&lt;/code&gt; value that was successfully read earlier never reaches the &lt;code&gt;Authorization&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;The request is then sent to several GitHub Traffic API endpoints, including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;traffic/views?per=day
traffic/clones?per=day
traffic/popular/referrers
traffic/popular/paths
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the API rejects the resulting authorization header, the workflow fails before the traffic data can be written to the CSV output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Is This Bug Easy to Miss?
&lt;/h2&gt;

&lt;p&gt;This finding highlights three distinct stages that should be considered when reviewing secrets in CI/CD pipelines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt; — Is the secret available to the workflow?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readability&lt;/strong&gt; — Does the application read the correct environment variable?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage&lt;/strong&gt; — Does the actual runtime value reach the external API boundary?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this case, the first two stages appeared correct. The bug only became visible at the third stage.&lt;/p&gt;

&lt;p&gt;This is why simply reviewing configuration files is not enough. We need to trace the data flow all the way to the outgoing request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Didn't the Existing Tests Catch It?
&lt;/h2&gt;

&lt;p&gt;I ran the repository's quality suite, which completed successfully with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;906 passed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test suite validates several important aspects of the repository, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;workflow-pack structure;&lt;/li&gt;
&lt;li&gt;scenario counts;&lt;/li&gt;
&lt;li&gt;skill metadata;&lt;/li&gt;
&lt;li&gt;local Markdown links;&lt;/li&gt;
&lt;li&gt;required automation files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, there was no test that invoked &lt;code&gt;api_get()&lt;/code&gt; with &lt;code&gt;urlopen&lt;/code&gt; mocked and then inspected the resulting request headers.&lt;/p&gt;

&lt;p&gt;In other words, the existing tests demonstrated that the repository structure and related metadata were valid, but they did not verify that the secret was actually used when the HTTP request was constructed.&lt;/p&gt;

&lt;p&gt;This is an important distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A test suite can provide strong structural coverage while still missing a critical runtime integration bug.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Recommended Fix
&lt;/h2&gt;

&lt;p&gt;The authorization header should use the runtime token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/vnd.github+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unvibecode-traffic-archiver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change itself is small, but it should be accompanied by a regression test.&lt;/p&gt;

&lt;p&gt;A suitable test should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;provide a dummy token through the environment;&lt;/li&gt;
&lt;li&gt;mock &lt;code&gt;urlopen&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;call &lt;code&gt;api_get()&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;inspect the generated request;&lt;/li&gt;
&lt;li&gt;verify that the &lt;code&gt;Authorization&lt;/code&gt; header contains the runtime token;&lt;/li&gt;
&lt;li&gt;ensure that the token is never printed to stdout or stderr;&lt;/li&gt;
&lt;li&gt;continue to verify that a missing token produces a clear error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The core assertion could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer test-token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual test should use a dummy value such as &lt;code&gt;test-token&lt;/code&gt;, never a real credential.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering Lessons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. A Secret Being Available Does Not Mean It Is Being Used
&lt;/h3&gt;

&lt;p&gt;An environment variable can be correctly configured and successfully read without ever reaching the component that needs it.&lt;/p&gt;

&lt;p&gt;For external integrations, the final boundary—such as an HTTP header, SDK argument, or subprocess invocation—must be explicitly verified.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Test the External Boundary, Not Just Internal Logic
&lt;/h3&gt;

&lt;p&gt;For code that communicates with an API, mock the network client or network function at the point where the request leaves the application.&lt;/p&gt;

&lt;p&gt;Verify at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP method;&lt;/li&gt;
&lt;li&gt;URL;&lt;/li&gt;
&lt;li&gt;authorization headers;&lt;/li&gt;
&lt;li&gt;relevant request parameters;&lt;/li&gt;
&lt;li&gt;and, where appropriate, request payloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach avoids making real API calls while keeping the test fast, deterministic, and safe.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Never Print Secrets for Debugging
&lt;/h3&gt;

&lt;p&gt;When inspecting authentication behavior, assert against the request object or sanitized metadata.&lt;/p&gt;

&lt;p&gt;Do not print the raw token into CI logs—even temporarily.&lt;/p&gt;

&lt;p&gt;A debugging statement such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can turn a functional debugging session into a credential-exposure incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Analysis Tools Help Trace the Path, but They Do Not Replace Verification
&lt;/h3&gt;

&lt;p&gt;UnvibeCode can make it easier to connect workflows, files, and dependencies and identify the path a secret takes through a repository.&lt;/p&gt;

&lt;p&gt;However, the final conclusion still needs to be validated against the actual source code and, where possible, a test at the runtime boundary.&lt;/p&gt;

&lt;p&gt;Static analysis can show that a variable is present.&lt;/p&gt;

&lt;p&gt;A boundary test can show that its value is actually transmitted.&lt;/p&gt;

&lt;p&gt;Those are different guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This is not an example of a secret being leaked into the repository. The problem is almost the opposite: &lt;strong&gt;the configured secret was never actually used.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The secret was not exposed.&lt;/li&gt;
&lt;li&gt;The API request could nevertheless fail.&lt;/li&gt;
&lt;li&gt;The workflow appeared to be correctly configured.&lt;/li&gt;
&lt;li&gt;The existing tests could still pass because they did not inspect the outgoing request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When auditing CI/CD pipelines, don't stop at the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Is the secret configured?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask the more important question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Does the secret actually reach the external boundary where it is required?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That boundary is often where a seemingly correct configuration turns into a runtime failure.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/FinanceFlash/unvibecode" rel="noopener noreferrer"&gt;UnvibeCode on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/hamznabil/secure-api-key-handling-in-python-projects-1kg7"&gt;Secure API Key Handling in Python Projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gitguardian/how-to-handle-secrets-in-python-c0n"&gt;How to Handle Secrets in Python&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; This article was written with AI assistance for language editing and presentation. The technical finding, repository inspection, and final conclusions are based on analysis of the source code and the repository's test results.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>cicd</category>
      <category>debugging</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Sun, 04 Oct 2026 00:47:19 +0000</pubDate>
      <link>https://dev.to/bangkah/-1hip</link>
      <guid>https://dev.to/bangkah/-1hip</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bangkah/building-a-privacy-first-ai-crm-assistant-for-my-friends-business-58i7" class="crayons-story__hidden-navigation-link"&gt;Building a Privacy-First AI CRM Assistant for My Friend's Business&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bangkah" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3665512%2Fdc6c5db5-91c9-4887-a5c6-33c8db391e07.jpeg" alt="bangkah profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bangkah" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Muhammad Dhiyaul Atha
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Muhammad Dhiyaul Atha
                
                
              
              &lt;div id="story-author-preview-content-4793577" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bangkah" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3665512%2Fdc6c5db5-91c9-4887-a5c6-33c8db391e07.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Muhammad Dhiyaul Atha&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bangkah/building-a-privacy-first-ai-crm-assistant-for-my-friends-business-58i7" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bangkah/building-a-privacy-first-ai-crm-assistant-for-my-friends-business-58i7" id="article-link-4793577"&gt;
          Building a Privacy-First AI CRM Assistant for My Friend's Business
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/weekendchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;weekendchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hf26challenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hf26challenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hacktoberfest"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hacktoberfest&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bangkah/building-a-privacy-first-ai-crm-assistant-for-my-friends-business-58i7" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bangkah/building-a-privacy-first-ai-crm-assistant-for-my-friends-business-58i7#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>privacy</category>
      <category>startup</category>
    </item>
    <item>
      <title>Building a Privacy-First AI CRM Assistant for My Friend's Business</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Sat, 03 Oct 2026 21:20:23 +0000</pubDate>
      <link>https://dev.to/bangkah/building-a-privacy-first-ai-crm-assistant-for-my-friends-business-58i7</link>
      <guid>https://dev.to/bangkah/building-a-privacy-first-ai-crm-assistant-for-my-friends-business-58i7</guid>
      <description>&lt;p&gt;Matt runs a growing business, but his daily intake operation was becoming a bottleneck. &lt;/p&gt;

&lt;p&gt;His team receives business inquiries through email, website forms, and messaging channels. The incoming information is highly inconsistent. Some inquiries are valuable sales opportunities, some are support questions, some are junk, and others simply lack enough information to make a decision.&lt;/p&gt;

&lt;p&gt;He needed a system to ingest these inquiries, identify what they are, extract useful structured information, and draft the next response. However, sending sensitive client business data to closed-source cloud LLMs was a major privacy concern.&lt;/p&gt;

&lt;p&gt;For the &lt;strong&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/strong&gt;, I decided to build him a secure, locally hosted &lt;strong&gt;AI CRM Intake Automation System&lt;/strong&gt; written in Go.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I designed a practical backend system that ingests incoming messages, uses local open-source AI to classify them, extracts structured data, and prepares the next steps for a human to review. &lt;/p&gt;

&lt;p&gt;Here is what the system does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingestion &amp;amp; Classification:&lt;/strong&gt; Determines if a message is a &lt;code&gt;Sales Lead&lt;/code&gt;, &lt;code&gt;Support Ticket&lt;/code&gt;, &lt;code&gt;Junk&lt;/code&gt;, or &lt;code&gt;Incomplete&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extraction:&lt;/strong&gt; Pulls out names, contact info, and business needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drafting:&lt;/strong&gt; Prepares a response based on the classification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-Loop:&lt;/strong&gt; Alerts Matt's team to review and approve the action before any CRM record is updated or any email is sent.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The prototype can process an inquiry locally and produce structured output for the next stage of the workflow.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;You can check out the project and its code structure here:&lt;br&gt;
&lt;a href="https://github.com/Bangkah/CRM-Automation-System" rel="noopener noreferrer"&gt;GitHub: CRM Automation System&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I built the workflow engine in Go around a simple principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The AI analyzes the data, but deterministic code executes the actions.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The LLM is responsible for classification, extraction, and drafting. It does not directly send emails, modify CRM records, or execute arbitrary actions. The deterministic Go backend handles validation, duplicate protection, idempotency, database updates, and the audit trail.&lt;/p&gt;
&lt;h3&gt;
  
  
  Human-in-the-Loop
&lt;/h3&gt;

&lt;p&gt;Consequential actions require human approval. Before an email is sent or a CRM record is updated, the workflow pauses and creates a pending approval for the operator to review.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/workflow/approval_service.go&lt;/span&gt;
&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ActionService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ApproveAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;actionID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;approverID&lt;/span&gt; &lt;span class="kt"&gt;string&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="n"&gt;ActionRecord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actionID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionRecord&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;ActionStatePendingApproval&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionRecord&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"invalid approval transition from %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&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="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;
    &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ActionStateApproved&lt;/span&gt;
    &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApprovedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;approverID&lt;/span&gt;
    &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionRecord&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;createAuditEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InquiryID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"approval.granted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;DecisionAllow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"approval"&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="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ActionRecord&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this project, using open-source AI locally through Ollama wasn't just a technical choice; it was a business requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Privacy
&lt;/h3&gt;

&lt;p&gt;Business inquiries can contain sensitive information such as budgets and personal contact details. By keeping AI inference local, the system does not need to send that data to a third-party LLM provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost Control
&lt;/h3&gt;

&lt;p&gt;Running an open-weight model locally avoids per-token API costs for every inquiry, including messages that eventually turn out to be spam or incomplete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Control and Predictability
&lt;/h3&gt;

&lt;p&gt;Running the model locally gives me more control over the model, prompting, and output format. The Go backend validates the structured JSON response before processing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This project reinforced my interest in building AI systems where models are useful for analysis and drafting, while deterministic software and human approval remain responsible for consequential actions.&lt;/p&gt;

&lt;p&gt;Happy hacking! 🌟&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>hacktoberfest</category>
    </item>
    <item>
      <title>My Hacktoberfest 2026 Journey: Contributing to DevOps Daily</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Thu, 01 Oct 2026 20:03:26 +0000</pubDate>
      <link>https://dev.to/bangkah/diving-into-hacktoberfest-2026-my-open-source-contribution-journey-on-devops-daily-6go</link>
      <guid>https://dev.to/bangkah/diving-into-hacktoberfest-2026-my-open-source-contribution-journey-on-devops-daily-6go</guid>
      <description>&lt;p&gt;Hacktoberfest is back, and this year I decided to join the 7-Day DevOps Contribution Challenge hosted by &lt;a class="mentioned-user" href="https://dev.to/devopsdaily"&gt;@devopsdaily&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;I’m still learning my way around open-source contribution workflows, so I’m using this challenge as an opportunity to practice working with real repositories, following existing project conventions, and getting more comfortable with the Git workflow.&lt;/p&gt;

&lt;p&gt;This article documents my progress so far, including how I tested my changes locally and the pull requests I submitted along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 1: Registering as an Expert on DevOps Daily
&lt;/h2&gt;

&lt;p&gt;On Day 1, the challenge focused on adding a personal expert profile to the platform's repository. Here’s what I did:&lt;/p&gt;

&lt;h5&gt;
  
  
  1. &lt;strong&gt;Creating the JSON Profile&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I created a data file at &lt;code&gt;content/experts/muhammad-dhiyaul-atha.json&lt;/code&gt; containing my professional background, social links, and core specialties in backend development, containerization, and automated deployment infrastructure.&lt;/p&gt;

&lt;h5&gt;
  
  
  2. &lt;strong&gt;Preparing the Avatar&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I uploaded my avatar image in &lt;code&gt;.png&lt;/code&gt; format to &lt;code&gt;public/images/experts/muhammad-dhiyaul-atha.png&lt;/code&gt; to ensure the profile renders correctly.&lt;/p&gt;

&lt;h5&gt;
  
  
  3. &lt;strong&gt;Local Testing&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I ran the local development server using &lt;code&gt;pnpm dev&lt;/code&gt; to verify that the system successfully read the files.&lt;/p&gt;

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

&lt;h5&gt;
  
  
  4. &lt;strong&gt;Opening the Pull Request (PR)&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;After confirming everything was clean, I created a dedicated branch, committed, and pushed the changes:&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;checkout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hacktoberfest/add-muhammad-dhiyaul-atha&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;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;content/experts/muhammad-dhiyaul-atha.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;public/images/experts/muhammad-dhiyaul-atha.png&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;commit&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="s2"&gt;"Add Muhammad Dhiyaul Atha to experts directory"&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;push&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hacktoberfest/add-muhammad-dhiyaul-atha&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then opened &lt;strong&gt;PR #1790&lt;/strong&gt; on &lt;a href="https://github.com/The-DevOps-Daily/devops-daily/pull/1790" rel="noopener noreferrer"&gt;@thedevopsdaily&lt;/a&gt;.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Day 2: Contributing a Favorite DevOps Tool (Lazydocker)
&lt;/h2&gt;

&lt;p&gt;For Day 2, the challenge was to modify a TypeScript component and add a new tool to the curated &lt;strong&gt;Toolbox&lt;/strong&gt;.&lt;/p&gt;

&lt;h5&gt;
  
  
  1. &lt;strong&gt;Choosing the Tool&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I chose Lazydocker, a Go-based terminal user interface (TUI) that makes managing Docker containers and Docker Compose easier from the command line.&lt;/p&gt;

&lt;h5&gt;
  
  
  2. &lt;strong&gt;Modifying &lt;code&gt;app/toolbox/page.tsx&lt;/code&gt;&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I added a new configuration object to the &lt;code&gt;tools&lt;/code&gt; array under the &lt;code&gt;containers&lt;/code&gt; category:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&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;Lazydocker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&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;A simple terminal UI for both docker and docker-compose, written in Go.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;href&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://github.com/jesseduffield/lazydocker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;containers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Terminal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;badges&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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Docker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outline&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="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Open Source&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secondary&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="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  3. &lt;strong&gt;Submitting the Second PR&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;Following the same Git workflow, I created a new branch, pushed the changes, and opened &lt;strong&gt;PR #1807&lt;/strong&gt; on &lt;a href="https://github.com/The-DevOps-Daily/devops-daily/pull/1807" rel="noopener noreferrer"&gt;@thedevopsdaily&lt;/a&gt;.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Day 3: Adding a Quiz Question to the Linux Quiz
&lt;/h2&gt;

&lt;p&gt;On Day 3, the task was to contribute a multiple-choice question to an existing quiz file on the platform. Here’s what I did:&lt;/p&gt;

&lt;h5&gt;
  
  
  1. &lt;strong&gt;Selecting the Quiz File&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I chose &lt;code&gt;content/quizzes/linux-quiz.json&lt;/code&gt; and drafted an intermediate-level question focused on modern network troubleshooting in Linux using the &lt;code&gt;ss&lt;/code&gt; command.&lt;/p&gt;

&lt;h5&gt;
  
  
  2. &lt;strong&gt;Drafting the Question &amp;amp; Updating Metadata&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I added a new question object to the &lt;code&gt;questions&lt;/code&gt; array:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"linux-check-listening-ports"&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;"Checking Listening Ports"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which command-line tool is commonly used on modern Linux distributions to inspect socket statistics and display active listening ports along with their corresponding process names?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&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;"netstat -an"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ss -tulpn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"lsof -port"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"ps aux --ports"&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;"correctAnswer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"explanation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ss (socket statistics) is the modern, faster replacement for netstat. The flags -tulpn display TCP/UDP connections, listening sockets, and the numeric PIDs/program names holding them."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"difficulty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"intermediate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"points"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&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;I also updated the &lt;code&gt;totalPoints&lt;/code&gt; (increased to &lt;code&gt;202&lt;/code&gt;) and incremented the &lt;code&gt;intermediate&lt;/code&gt; difficulty level count in the file's metadata.&lt;/p&gt;

&lt;h5&gt;
  
  
  3. &lt;strong&gt;Local Testing &amp;amp; Validation&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;Before committing, I ran the repository's test and validation commands to verify the quiz schema and metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;test &lt;/span&gt;tests/quiz-validation.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm quiz:validate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h5&gt;
  
  
  4. &lt;strong&gt;Opening the Pull Request (PR)&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;After confirming everything passed successfully, I created a dedicated branch, committed, and pushed the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; hacktoberfest/add-quiz-question
git add content/quizzes/linux-quiz.json
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add quiz question on checking listening ports to linux-quiz"&lt;/span&gt;
git push origin hacktoberfest/add-quiz-question

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then opened the Pull Request on &lt;a href="https://github.com/The-DevOps-Daily/devops-daily/pull/1814" rel="noopener noreferrer"&gt;@thedevopsdaily&lt;/a&gt;.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Day 4: Adding Flashcards to Linux Fundamentals
&lt;/h2&gt;

&lt;p&gt;On Day 4, the objective was to contribute one or two study flashcards to an existing set to help others learn DevOps and system administration concepts. Here’s what I did:&lt;/p&gt;

&lt;h5&gt;
  
  
  1. &lt;strong&gt;Selecting the Flashcard Set&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I chose &lt;code&gt;content/flashcards/linux-fundamentals.json&lt;/code&gt; since it covers essential Linux topics and fits well with my background in Linux system administration.&lt;/p&gt;

&lt;h5&gt;
  
  
  2. &lt;strong&gt;Drafting and Adding the Flashcards&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I added two new flashcard objects to the end of the cards array, covering network troubleshooting with ss and systemd logs with journalctl:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"linux-check-listening-ports"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"front"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How do you inspect active listening ports and their processes on modern Linux?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"back"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Use the ss command with flags -tulpn (ss -tulpn). It is the modern, faster replacement for netstat, displaying TCP/UDP connections, listening sockets, and the numeric PIDs/program names holding them."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Networking"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&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;"ss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ports"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"networking"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"troubleshooting"&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="err"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"journalctl-systemd-logs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"front"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How do you query and follow systemd logs using journalctl?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"back"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"journalctl -u service-name views logs for a specific service. Add -f to follow live output (like tail -f), -n 50 for the last 50 lines, and --since today for recent logs. It reads binary journal logs maintained by systemd."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Monitoring"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"tags"&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;"journalctl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"systemd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"troubleshooting"&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;I also updated the &lt;code&gt;cardCount&lt;/code&gt; metadata at the top of the file, increasing it from &lt;code&gt;20&lt;/code&gt; to &lt;code&gt;22&lt;/code&gt;.&lt;/p&gt;

&lt;h5&gt;
  
  
  3. &lt;strong&gt;Local Testing &amp;amp; Preview&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;I started the local development server to test the flashcards page and ensure the new cards rendered correctly:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



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

&lt;h5&gt;
  
  
  4. &lt;strong&gt;Opening the Pull Request (PR)&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;After confirming everything passed successfully, I created a dedicated branch, committed, and pushed the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; hacktoberfest/add-flashcard
git add content/flashcards/linux-fundamentals.json
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add flashcard for checking listening ports using ss to linux-fundamentals"&lt;/span&gt;
git push origin hacktoberfest/add-flashcard

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then opened the Pull Request on &lt;a href="https://github.com/The-DevOps-Daily/devops-daily/pull/1828" rel="noopener noreferrer"&gt;@thedevopsdaily&lt;/a&gt;.&lt;/p&gt;

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




&lt;h2&gt;
  
  
  Day 5: Sharing a Practical DevOps Gotcha on CI/CD Security
&lt;/h2&gt;

&lt;p&gt;On Day 5, the objective shifted from adding static data (like quizzes or flashcards) to sharing a practical tip, "gotcha," or lesson learned the hard way by improving an existing guide on the platform.&lt;/p&gt;

&lt;h5&gt;
  
  
  1. Choosing the Right Article to Improve
&lt;/h5&gt;

&lt;p&gt;I browsed through &lt;code&gt;content/guides/introduction-to-cicd/&lt;/code&gt; and selected &lt;code&gt;09-security-best-practices-and-production-patterns.md&lt;/code&gt;. Having dealt firsthand with workflow permission issues while setting up automated tasks and repository stats updaters, I knew exactly what kind of pitfall often catches developers off guard.&lt;/p&gt;

&lt;h5&gt;
  
  
  2. Drafting the Gotcha Section
&lt;/h5&gt;

&lt;p&gt;Inside the section covering &lt;em&gt;Least Privilege Access&lt;/em&gt;, I inserted a concise and technically accurate warning regarding GitHub Actions default token permissions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; If your GitHub Actions workflow fails with a "403 Forbidden" error when trying to push commits or update repository files, check the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; permissions. Repositories can use read-only defaults for the token. If the workflow needs to write repository contents, explicitly grant the required permission, such as &lt;code&gt;contents: write&lt;/code&gt;, in the workflow.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h5&gt;
  
  
  3. Verifying and Isolating the Branch
&lt;/h5&gt;

&lt;p&gt;To keep my contribution clean and prevent stray build artifacts or previous commits from leaking into this pull request, I made sure to check out a dedicated branch from a clean state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git pull origin main
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; hacktoberfest/add-tip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  4. Committing and Opening the Pull Request
&lt;/h5&gt;

&lt;p&gt;After verifying the changes locally, I staged only the modified guide file, committed with a clear message, and pushed to my fork:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add content/guides/introduction-to-cicd/09-security-best-practices-and-production-patterns.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"docs: add github token permission gotcha"&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin hacktoberfest/add-tip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then opened the Pull Request on &lt;a href="https://github.com/The-DevOps-Daily/devops-daily/pull/1835" rel="noopener noreferrer"&gt;@thedevopsdaily&lt;/a&gt;.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So far, this challenge has been a useful way to practice the basics of contributing to an existing open-source project.&lt;/p&gt;

&lt;p&gt;The work itself has been relatively small, but there have still been things to learn: understanding an unfamiliar codebase, following its existing structure, testing changes locally, creating focused branches, and preparing pull requests.&lt;/p&gt;

&lt;p&gt;I’m still in the middle of the challenge, so there is a lot more to learn and contribute. I’ll continue updating this article as I work through the remaining days of the challenge.&lt;/p&gt;

&lt;p&gt;If you're also participating in Hacktoberfest this year, I'd be interested to hear what you've been working on.&lt;/p&gt;

&lt;p&gt;Happy hacking! 🌟&lt;/p&gt;

</description>
      <category>hacktoberfest</category>
      <category>devops</category>
      <category>opensource</category>
      <category>devopsdaily</category>
    </item>
    <item>
      <title>My Experience Deploying Projects Smoothly with Tencent EdgeOne Makers</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Sun, 13 Sep 2026 11:23:07 +0000</pubDate>
      <link>https://dev.to/bangkah/my-experience-deploying-projects-smoothly-with-tencent-edgeone-makers-epm</link>
      <guid>https://dev.to/bangkah/my-experience-deploying-projects-smoothly-with-tencent-edgeone-makers-epm</guid>
      <description>&lt;p&gt;Exploring modern cloud infrastructure, edge computing, and streamlined deployment platforms has always been an exciting journey for developers. Recently, as part of the DevHandal 2026 Batch 2 program, I had the opportunity to dive deep into Tencent EdgeOne Makers, and it has genuinely changed how I approach shipping web projects efficiently.&lt;/p&gt;

&lt;p&gt;In this article, I want to share my honest review, technical impressions, and why this platform is becoming an exceptional choice for developers looking for fast, secure, and hassle-free website deployment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Tencent EdgeOne Makers Stands Out
&lt;/h4&gt;

&lt;p&gt;When working on modern web applications, setting up Content Delivery Networks (CDNs), security layers like Web Application Firewalls (WAF), DDoS mitigation, and global edge routing usually takes a considerable amount of configuration overhead. Traditional setups often require juggling multiple services across different cloud providers.&lt;/p&gt;

&lt;p&gt;EdgeOne Makers simplifies all of that by consolidating these essential features into a unified, developer-friendly ecosystem. Some of the standout features that caught my attention during my exploration include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global Edge Performance:&lt;/strong&gt; Static and dynamic assets are cached and served closer to users worldwide. This architectural approach results in significantly lower latency, faster Time to First Byte (TTFB), and an overall instant page load experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in Security Protection:&lt;/strong&gt; Robust security measures come right out of the box. It keeps applications safe from common web vulnerabilities, malicious traffic, and DDoS attacks without requiring complex external plugins or expensive enterprise firewalls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streamlined Developer Workflow:&lt;/strong&gt; The integration pipeline allows developers to spin up, configure, and deploy applications rapidly, making it ideal for both rapid prototyping and production-grade applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Getting Started and Deployment Experience
&lt;/h4&gt;

&lt;p&gt;Deploying a project on the platform is remarkably straightforward. Whether you choose to manage your configurations via their intuitive command-line interface (CLI) or through the web dashboard, the process is streamlined to let developers focus purely on writing application code rather than wrestling with server maintenance.&lt;/p&gt;

&lt;p&gt;When I tested deploying my project, the workflow felt natural. Here is a quick breakdown of the steps and best practices I followed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clean Project Initialization:&lt;/strong&gt; Structuring project files logically so that the edge runtime can route static assets and serverless functions seamlessly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smooth CLI Commands:&lt;/strong&gt; Leveraging fast local testing tools and straightforward deployment commands that push code straight to the edge network in a matter of seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generous Free Tier and Quotas:&lt;/strong&gt; Having access to a robust free tier makes it stress-free to experiment, build, and showcase projects without worrying about unexpected upfront server costs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My overall experience using Tencent EdgeOne Makers has been fantastic. It successfully removes the friction and complexity of traditional server management, enabling developers to ship secure, high-performance applications with absolute ease. If you haven't tried it yet, I highly recommend exploring EdgeOne Makers for your next web project. It is a powerful tool that elevates both developer productivity and end-user performance. Furthermore, the supportive ecosystem and continuous updates make it a reliable choice for long-term scalability, ensuring that hobbyists and professional developers alike can build resilient web solutions with minimal hassle. Additionally, participating in this developer program has opened up new insights into modern edge architectures.&lt;/p&gt;

</description>
      <category>tencentedgeone</category>
      <category>edgeonemakers</category>
      <category>codepolitan</category>
      <category>edgeone</category>
    </item>
    <item>
      <title>Three Bugs That Almost Killed My Honeypot Before It Ever Caught Anyone</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Thu, 10 Sep 2026 09:30:55 +0000</pubDate>
      <link>https://dev.to/bangkah/three-bugs-that-almost-killed-my-honeypot-before-it-ever-caught-anyone-5a2g</link>
      <guid>https://dev.to/bangkah/three-bugs-that-almost-killed-my-honeypot-before-it-ever-caught-anyone-5a2g</guid>
      <description>&lt;p&gt;I built Bangk-Shield, an edge-native honeypot that runs on Cloudflare Workers. The idea is simple: sit in front of a site, watch for SQLi/RCE/SSRF/LFI/XSS/recon probes, and instead of quietly blocking them, answer with a convincing fake payload and a locally-flavored roast — while logging the real attacker's fingerprint.&lt;/p&gt;

&lt;p&gt;The pitch is fun. The bug hunt getting it to actually work was not. Here are three failures that taught me more about edge runtimes and silent regressions than any tutorial would have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #1: The Worker That Attacked Itself
&lt;/h2&gt;

&lt;p&gt;First local test run. I hit two endpoints with &lt;code&gt;curl&lt;/code&gt; and watched the &lt;code&gt;wrangler dev&lt;/code&gt; log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[wrangler:info] GET /etc/passwd 500 Internal Server Error (13820ms)
[wrangler:info] GET /.env 500 Internal Server Error (13833ms)
[wrangler:info] GET /etc/passwd 500 Internal Server Error (13831ms)
[wrangler:info] GET /.env 500 Internal Server Error (13844ms)
...hundreds more lines...
[wrangler:info] GET /etc/passwd 500 Internal Server Error (20822ms)
⎔ Shutting down local server...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;curl&lt;/code&gt; calls. Hundreds of log lines. Latency climbing from 13.8 seconds to 20.8 seconds before the dev server gave up and died. That climb is the tell — nothing about a single HTTP request naturally gets 7 seconds slower over its own lifetime. Something was calling itself.&lt;/p&gt;

&lt;p&gt;The culprit was in my "passthrough" logic — the code path that forwards legitimate traffic to the real origin server:&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="c1"&gt;// The bug&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;passthrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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="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="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;ASSETS&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="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- this&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In local dev, with no real origin configured, &lt;code&gt;fetch(request)&lt;/code&gt; re-fetches the &lt;em&gt;exact same URL&lt;/em&gt; — which, in &lt;code&gt;wrangler dev&lt;/code&gt;, is the Worker's own address. The Worker calls itself. That call also fails to resolve a real backend, hits the same fail-open catch block, and calls &lt;code&gt;fetch(request)&lt;/code&gt; again. Recursion, with no base case, burning CPU and stacking subrequests until the runtime hit a limit and returned 500.&lt;/p&gt;

&lt;p&gt;The scary part isn't that it happened in dev. It's that this same failure mode is a documented Cloudflare Workers gotcha in production too, if a Worker's outbound fetch happens to match one of its own routes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt; had three parts, because I wanted defense in depth, not just a patch:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;passthrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Loop guard — if we already forwarded this request once, stop.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-bangk-shield-forwarded&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bangk-Shield: forwarding loop detected.&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;508&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="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;ASSETS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;ASSETS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Explicit origin — never re-fetch the same host as this Worker.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;ORIGIN_URL&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;target&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;URL&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;ORIGIN_URL&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;forwardUrl&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;forwardUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;forwardUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;forwardUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&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;forwarded&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;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;forwardUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;forwarded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-bangk-shield-forwarded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;forwarded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. No origin configured — say so, instead of guessing and looping.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bangk-Shield is active, but no ORIGIN_URL is configured for legitimate traffic.&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lesson: a "safe default" that just re-fetches the incoming request isn't safe. It's an assumption that something &lt;em&gt;else&lt;/em&gt; out there is going to intercept and redirect the call before it loops. In a reverse-proxy pattern, that assumption needs to be an explicit contract (an origin URL, a binding), not implicit behavior you hope the runtime handles for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #2: The Detector That Never Detected Path Attacks
&lt;/h2&gt;

&lt;p&gt;This one didn't crash anything. It just silently did nothing, which is worse.&lt;/p&gt;

&lt;p&gt;After a rewrite that moved detection rules into a scored, combo-bonus rule engine, I built the request context like this:&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="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safeDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&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;Looks reasonable. Except: &lt;strong&gt;there's no &lt;code&gt;path&lt;/code&gt; field.&lt;/strong&gt; And the rule engine matches signals against &lt;code&gt;context[target]&lt;/code&gt;, where &lt;code&gt;target&lt;/code&gt; comes from each signal's &lt;code&gt;where&lt;/code&gt; array in the rules config — things like &lt;code&gt;"where": ["path", "query", "body"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every rule that targeted &lt;code&gt;path&lt;/code&gt; — &lt;code&gt;/etc/passwd&lt;/code&gt;, &lt;code&gt;/.env&lt;/code&gt;, &lt;code&gt;/wp-admin&lt;/code&gt;, all the classic recon and LFI probes that live in the URL path rather than the query string — evaluated &lt;code&gt;context['path']&lt;/code&gt;, got &lt;code&gt;undefined&lt;/code&gt;, and silently failed to match. No error. No warning. The honeypot just let path-based attacks walk straight through to the real origin, forever, while confidently reporting "no threats detected."&lt;/p&gt;

&lt;p&gt;This is the kind of bug that unit tests catch and manual clicking doesn't, because manually poking &lt;code&gt;/?id=1' OR 1=1&lt;/code&gt; in the query string "worked," so the feature "looked done."&lt;/p&gt;

&lt;p&gt;The fix was one line:&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="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safeDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- this was missing&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safeDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&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;But the fix isn't really the interesting part — the &lt;em&gt;verification&lt;/em&gt; is. I wrote a small standalone test harness that fed known attack shapes straight into the scoring function and asserted whether they crossed the trigger threshold:&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="nf"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/etc/passwd in path (must trigger alone)&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/etc/passwd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;curl/8.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/.env alone (score too low, must pass through)&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="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/.env&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;curl/8.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&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="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running that harness against the &lt;em&gt;old&lt;/em&gt; context shape would have failed instantly and obviously — instead of failing silently in production traffic six months later. Lesson: for a security tool, "it compiles and the demo works" is not a test suite. If a detector can fail closed (i.e., fail into "everything looks safe"), you need an explicit assertion that proves the opposite case still fires.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #3: The Watermark That Lied
&lt;/h2&gt;

&lt;p&gt;Every honeypot response includes a cryptographic watermark — a hash derived from the visitor's IP, User-Agent, date, and a rotating salt. The point is defensive: if someone screenshots a fake "leaked database" response and claims it's a real breach, the site owner can reproduce the hash and prove it's a decoy.&lt;/p&gt;

&lt;p&gt;The spec (written into the project's PRD) was explicit about the exact format, because a mismatch means the hash can never be reproduced later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Canonical: bangk-shield/v1|&amp;lt;ip&amp;gt;|&amp;lt;ua&amp;gt;|&amp;lt;yyyy-mm-dd&amp;gt;|&amp;lt;salt&amp;gt;
Output:    bs1-&amp;lt;first 16 hex chars of SHA-256(canonical)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shipped code did this instead:&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="nx"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`bangk-shield|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dateStr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// missing /v1&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`bs-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hashHex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                              &lt;span class="c1"&gt;// missing the 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small diff. Big consequence: any CI test vector built against the spec format would fail against this code forever, and worse, nobody would notice until someone tried to reproduce a watermark from a real incident and got a hash that simply didn't match anything.&lt;/p&gt;

&lt;p&gt;The second half of this bug was sneakier. The spec said the salt should live in Workers KV as a &lt;strong&gt;rotatable object&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wm:salt:current&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;"salt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a1b2c3..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"since"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-08T10:00:00Z"&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;But the code read the KV value and used it &lt;em&gt;directly&lt;/em&gt; as the salt string:&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="nx"&gt;salt&lt;/span&gt; &lt;span class="o"&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;WATERMARK_SALT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;default-dev-salt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// never even reads KV&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two problems stacked on top of each other: the salt wasn't actually coming from KV at all (so "rotate without redeploy" didn't work), and if I'd naively "fixed" that by just doing &lt;code&gt;await env.BANGK_KV.get('wm:salt:current')&lt;/code&gt;, the salt would have become the literal string &lt;code&gt;'{"salt":"a1b2c3...","since":"..."}'&lt;/code&gt; — every watermark computed with the &lt;em&gt;entire JSON blob&lt;/em&gt; as the salt, silently, with no error thrown anywhere.&lt;/p&gt;

&lt;p&gt;The fix parses defensively and stays backward-compatible with plain strings during migration:&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;function&lt;/span&gt; &lt;span class="nf"&gt;extractSaltValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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;raw&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// valid JSON, wrong shape — treat as a plain salt&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// not JSON at all — legacy plain-string salt&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="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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;Lesson: any time two independent things (a spec doc and an implementation, or a producer and a consumer of the same data) both need to agree on a format, that agreement should be enforced somewhere machine-checkable — a schema, a fixed test vector, a shared constant — not just "well-documented." Documentation drifts. Code that has to pass a byte-for-byte comparison doesn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Changed My Process
&lt;/h2&gt;

&lt;p&gt;None of these three bugs were exotic. They were all boring, one-line-diff mistakes. What made them dangerous was that each one &lt;strong&gt;failed silently&lt;/strong&gt; — the Worker kept returning 200s, the demo kept looking fine, and the failure mode only showed up under conditions I hadn't manually tested (a request with no real origin behind it, an attack in the path instead of the query, a salt fetched from the "real" storage instead of the dev fallback).&lt;/p&gt;

&lt;p&gt;Three things I do differently now, on this project and since:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fail-closed the build, fail-open the runtime.&lt;/strong&gt; A honeypot must never take down the site it's protecting — so the request path stays fail-open (any internal error just forwards traffic). But the &lt;em&gt;build pipeline&lt;/em&gt; that compiles detection rules now fails hard on anything suspicious: invalid regex, patterns that can never match because of a later &lt;code&gt;.toLowerCase()&lt;/code&gt;, ReDoS-prone patterns. Silent failure in the runtime is safe; silent failure in the config is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the shape of the data, not just the happy path.&lt;/strong&gt; The path-detection bug and the salt-shape bug were both "the code runs, produces a 200, and does the wrong thing." A quick harness that asserts on expected trigger/no-trigger outcomes for known attack payloads catches this class of bug in seconds instead of after deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat spec-vs-code drift as a bug class of its own.&lt;/strong&gt; Once I had a real PRD with byte-exact format requirements, I started explicitly diffing "what the spec says" against "what the code does" as a review step — not just reading the code in isolation and asking "does this look reasonable?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is still in beta. But it's a beta that now has a test harness, a build-time linter that can fail on purpose, and a much shorter list of ways it can lie to me about whether it's actually working.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>debugging</category>
      <category>security</category>
    </item>
    <item>
      <title>Membangun Infrastruktur Enterprise: Part 2 - Sinkronisasi Waktu Server dengan NTP (Chrony)</title>
      <dc:creator>Muhammad Dhiyaul Atha</dc:creator>
      <pubDate>Thu, 11 Jun 2026 13:26:59 +0000</pubDate>
      <link>https://dev.to/bangkah/membangun-infrastruktur-enterprise-part-2-sinkronisasi-waktu-server-dengan-ntp-chrony-29no</link>
      <guid>https://dev.to/bangkah/membangun-infrastruktur-enterprise-part-2-sinkronisasi-waktu-server-dengan-ntp-chrony-29no</guid>
      <description>&lt;p&gt;Setelah berhasil melakukan konfigurasi dasar pada &lt;em&gt;Main Server&lt;/em&gt; di Part 1, langkah krusial selanjutnya sebelum mengonfigurasi Active Directory (ADDS) adalah memastikan waktu pada sistem server benar-benar akurat. Artikel ini akan membahas langkah demi langkah instalasi dan konfigurasi &lt;strong&gt;NTP (Network Time Protocol) menggunakan Chrony&lt;/strong&gt; di &lt;strong&gt;Ubuntu Server 26.04&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Teori Dasar: Mengapa NTP Sangat Krusial Sebelum ADDS?
&lt;/h3&gt;

&lt;p&gt;Banyak administrator jaringan pemula melewatkan konfigurasi NTP dan langsung melompat ke instalasi Active Directory. Hal ini sering kali berujung pada kegagalan sistem yang membingungkan.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protokol Kerberos:&lt;/strong&gt; Active Directory Domain Services (ADDS) menggunakan protokol &lt;strong&gt;Kerberos&lt;/strong&gt; untuk proses autentikasi keamanan. Kerberos menggunakan stempel waktu (&lt;em&gt;timestamp&lt;/em&gt;) untuk mencegah serangan siber jenis &lt;em&gt;replay attacks&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Toleransi Batas Waktu (Time Skew):&lt;/strong&gt; Secara bawaan, Kerberos membatasi perbedaan waktu maksimal antara Domain Controller, server, dan komputer &lt;em&gt;client&lt;/em&gt; sebesar &lt;strong&gt;5 menit&lt;/strong&gt;. Jika perbedaan waktu melebihi ambang batas ini, proses login user, replikasi &lt;em&gt;database&lt;/em&gt; domain, hingga proses &lt;em&gt;Join Domain&lt;/em&gt; pada komputer klien akan &lt;strong&gt;pasti ditolak dan gagal&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Oleh karena itu, server harus bertindak sebagai sumber waktu yang andal (&lt;em&gt;NTP Server&lt;/em&gt;) bagi seluruh perangkat di dalam jaringan &lt;code&gt;a3n4.com&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Sintaks &amp;amp; Langkah Konfigurasi
&lt;/h3&gt;

&lt;p&gt;Pastikan Anda sudah berada di terminal Ubuntu Server 26.04 dengan hak akses &lt;em&gt;root&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Langkah A: Menonaktifkan NTP Bawaan Ubuntu
&lt;/h4&gt;

&lt;p&gt;Secara bawaan, Ubuntu menggunakan layanan &lt;code&gt;systemd-timesyncd&lt;/code&gt; untuk sinkronisasi waktu internal. Karena kita ingin server ini bertindak sebagai &lt;em&gt;NTP Server&lt;/em&gt; mandiri (melayani &lt;em&gt;request&lt;/em&gt; dari jaringan lokal), kita harus mematikan layanan bawaan tersebut.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timedatectl set-ntp off

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Langkah B: Instalasi Chrony
&lt;/h4&gt;

&lt;p&gt;Chrony adalah aplikasi NTP modern yang sangat direkomendasikan untuk Linux karena proses sinkronisasinya yang lebih cepat dan presisi dibanding &lt;em&gt;daemon&lt;/em&gt; NTP lama.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install &lt;/span&gt;chrony &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Langkah C: Konfigurasi Chrony Server
&lt;/h4&gt;

&lt;p&gt;Buka dan edit file konfigurasi utama Chrony menggunakan &lt;code&gt;nano&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano /etc/chrony/chrony.conf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tambahkan baris berikut di bagian bawah file untuk mengizinkan jaringan lokal &lt;code&gt;192.168.190.0/24&lt;/code&gt; menyalin waktu dari server ini, serta memaksa server tetap memberikan waktu meskipun koneksi internet terputus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;allow 192.168.190.0/24
local stratum 10

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simpan file (&lt;code&gt;Ctrl + O&lt;/code&gt;, lalu &lt;code&gt;Enter&lt;/code&gt;) dan keluar (&lt;code&gt;Ctrl + X&lt;/code&gt;).&lt;/p&gt;

&lt;h4&gt;
  
  
  Langkah D: Restart &amp;amp; Aktivasi Layanan
&lt;/h4&gt;

&lt;p&gt;Terapkan konfigurasi baru dengan merestart layanan Chrony dan pastikan statusnya berjalan otomatis saat &lt;em&gt;booting&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;chrony
systemctl restart chrony
systemctl status chrony

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3. Pengujian &amp;amp; Verifikasi
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Langkah A: Memeriksa Port NTP (123)
&lt;/h4&gt;

&lt;p&gt;NTP berjalan di atas protokol &lt;strong&gt;UDP port 123&lt;/strong&gt;. Pastikan socket port tersebut sudah terbuka dan didengarkan (&lt;em&gt;listen&lt;/em&gt;) oleh Chrony.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-unlp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;123

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Langkah B: Verifikasi Sumber Waktu &amp;amp; Pelacakan
&lt;/h4&gt;

&lt;p&gt;Pastikan Chrony sukses terhubung ke server waktu publik di internet untuk menyinkronkan jam internalnya.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chronyc sources
chronyc tracking

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Kesimpulan
&lt;/h3&gt;

&lt;p&gt;Dengan menyelesaikan konfigurasi NTP menggunakan Chrony, kita telah memastikan bahwa jam internal pada Ubuntu Server 26.04 berjalan dengan akurasi tinggi dan siap melayani permintaan sinkronisasi dari komputer &lt;em&gt;client&lt;/em&gt;. Sinkronisasi waktu ini meminimalkan risiko kegagalan otentikasi di masa mendatang.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apa Selanjutnya?
&lt;/h3&gt;

&lt;p&gt;Waktu server sudah akurat dan siap. Di artikel berikutnya (Part 3), kita akan masuk ke tahap paling krusial dari seluruh proyek ini: &lt;strong&gt;Membangun Active Directory Domain Services (ADDS)&lt;/strong&gt; menggunakan Samba untuk menginisiasi domain utama kita, &lt;strong&gt;&lt;code&gt;a3n4.com&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.google.com/search?q=%23" rel="noopener noreferrer"&gt;Baca Part 3: Membangun Active Directory Domain Controller (ADDS) dengan Samba ➡️&lt;/a&gt;&lt;/strong&gt; &lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>linux</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
