[{"data":1,"prerenderedAt":333},["ShallowReactive",2],{"site":3,"writing-\u002Fwriting\u002Fhow-to-audit-a-system-with-an-ai-agent-that-never-gets-the-keys":50,"writing-nav":320},{"id":4,"bookingLabel":5,"bookingUrl":6,"company":7,"contact":8,"extension":11,"hire":12,"legal":15,"location":16,"meta":17,"name":18,"nav":19,"owner":38,"social":39,"status":46,"stem":47,"tagline":48,"__hash__":49},"site\u002Fsite.yml","Book a call for what's next","https:\u002F\u002Fcal.com\u002Fanthive\u002Fintro","Ant Hive d.o.o.",{"heading":9,"intro":10},"Start with a call.","Thirty minutes, no slides. Tell me what you're building and when you need someone.","yml",{"heading":13,"intro":14},"If you're here to hire","Start with the problem you have. I'll tell you straight whether I'm the right person for it.","Ant Hive d.o.o., Braće Radića 41, Solin, Croatia. OIB 87072518531. MBS 05985889, Trgovački sud u Splitu. Share capital € 2.500, paid in full. Director: Ante Borzić.","Split, Croatia, CET",{},"Ant Hive",[20,23,26,29,32,35],{"label":21,"to":22},"Writing","\u002Fwriting",{"label":24,"to":25},"Work with me","\u002Fwork-with-me",{"label":27,"to":28},"On my desk","\u002Fdesk",{"label":30,"to":31},"About","\u002Fabout",{"label":33,"to":34},"Colophon","\u002Fcolophon",{"label":36,"to":37},"Contact","\u002Fcontact","Ante Borzić",[40,43],{"label":41,"to":42},"LinkedIn","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fante-borzic",{"label":44,"to":45},"GitHub","https:\u002F\u002Fgithub.com\u002Fb0rza","On a long-term contract.","site","Writing code is the easy part now. Musings and work from Ante Borzić, on the rest.","2Xoi9K5fmdspVs2Iabkuc-XV6mG1EfiZBm3RojZ-9K0",{"id":51,"title":52,"body":53,"date":307,"description":308,"draft":309,"extension":192,"headline":310,"meta":314,"minutes":139,"navigation":205,"path":315,"seo":316,"stem":317,"topic":318,"__hash__":319},"writing\u002Fwriting\u002Fhow-to-audit-a-system-with-an-ai-agent-that-never-gets-the-keys.md","How to audit a system with an AI agent that never gets the keys",{"type":54,"value":55,"toc":298},"minimark",[56,60,63,68,84,88,91,94,98,101,155,158,164,175,178,181,185,188,233,236,239,243,246,249,252,266,270,273,284,287,291,294],[57,58,59],"p",{},"By the end of this setup, a team auditing a system it didn't build has a report in layers, from one page to a full write-up, where every claim points to a file. The reading is done by a coding agent that never touches a live system and never sees a credential.",[57,61,62],{},"The agent is good at the slow part: reading thousands of lines of metadata, tickets and wiki pages, and keeping track of what it found. It's also the riskiest reader you could hand a token to. Tickets and wiki pages are text anyone could have written, and an agent that holds a live credential can be steered by what it reads. So the work splits cleanly: people touch the systems, the agent touches files.",[64,65,67],"h2",{"id":66},"what-you-need-before-starting","What you need before starting",[69,70,71,75,78,81],"ul",{},[72,73,74],"li",{},"A read-only account on each system you're auditing. Ask the owner for read-only access on day one. If they can only give you admin, that's your first finding.",[72,76,77],{},"The OS keychain, to hold the tokens.",[72,79,80],{},"A git repo for the audit itself: plan, briefs, findings and report.",[72,82,83],{},"A coding agent whose permissions you can restrict: no network, no platform command-line tools, and file access only inside the repo.",[64,85,87],{"id":86},"write-the-plan-and-the-tracker-first","Write the plan and the tracker first",[57,89,90],{},"The plan splits the audit into workstreams: preparation, technical, risk, operations and the report. Each workstream is a list of steps, and the tracker says which steps are done, in progress or waiting on someone outside the team.",[57,92,93],{},"Add a glossary at the same time. An inherited system has its own names for things, and the agent uses whichever name it saw last. A glossary in the repo keeps the agent and the people on the same words.",[64,95,97],{"id":96},"run-every-export-yourself","Run every export yourself",[57,99,100],{},"Anything that talks to a live system is a person's job. Each export is a small script that a person runs, and it reads its token from the keychain at that moment:",[102,103,108],"pre",{"className":104,"code":105,"language":106,"meta":107,"style":107},"language-sh shiki shiki-themes github-light github-dark","#!\u002Fbin\u002Fsh\n# Run by a person, never by the agent.\nset -eu\ntoken=$(security find-generic-password -s audit-tickets -w)\ncurl -fsS -H \"Authorization: Bearer $token\" \\\n  \"https:\u002F\u002Ftickets.example.com\u002Fapi\u002Fsearch?updated-since=2026-01-01\" \\\n  > \"inputs\u002Ftickets-production-$(date +%F).json\"\n","sh","",[109,110,111,119,125,131,137,143,149],"code",{"__ignoreMap":107},[112,113,116],"span",{"class":114,"line":115},"line",1,[112,117,118],{},"#!\u002Fbin\u002Fsh\n",[112,120,122],{"class":114,"line":121},2,[112,123,124],{},"# Run by a person, never by the agent.\n",[112,126,128],{"class":114,"line":127},3,[112,129,130],{},"set -eu\n",[112,132,134],{"class":114,"line":133},4,[112,135,136],{},"token=$(security find-generic-password -s audit-tickets -w)\n",[112,138,140],{"class":114,"line":139},5,[112,141,142],{},"curl -fsS -H \"Authorization: Bearer $token\" \\\n",[112,144,146],{"class":114,"line":145},6,[112,147,148],{},"  \"https:\u002F\u002Ftickets.example.com\u002Fapi\u002Fsearch?updated-since=2026-01-01\" \\\n",[112,150,152],{"class":114,"line":151},7,[112,153,154],{},"  > \"inputs\u002Ftickets-production-$(date +%F).json\"\n",[57,156,157],{},"The token never sits in an env file or in the repo, so there's nothing for the agent to find.",[159,160,161],"sidenote",{},[57,162,163],{},"A big export, like a whole wiki space, gets a catalog first: an index of what's in it by topic and date. Briefs then point at slices of it instead of at everything.",[57,165,166,167,170,171,174],{},"Exports go into an ",[109,168,169],{},"inputs\u002F"," folder listed in ",[109,172,173],{},".gitignore",". Each file name says which system, which environment and which day it came from. Two environments of the same system look almost identical in an export, and a finding about the wrong one is worse than no finding.",[57,176,177],{},"Export metadata and configuration, not records. You need to know that a field holds email addresses, not the addresses themselves. Where records can't be avoided, write that down, because the inputs folder now holds personal data.",[57,179,180],{},"Then set the agent's permissions to match. Deny network access and the platform's command-line tools, and allow file access only inside the repo. The agent can still run summary scripts you give it over files already on disk. It just can't reach anything live.",[64,182,184],{"id":183},"one-brief-per-area-before-any-findings","One brief per area, before any findings",[57,186,187],{},"Each area of the audit gets a brief before the agent reads anything for it. The brief says which question the area answers, which inputs to read, what's out of scope, what a finding looks like and when the area is done:",[102,189,193],{"className":190,"code":191,"language":192,"meta":107,"style":107},"language-md shiki shiki-themes github-light github-dark","# Brief: connections and logins\n\nQuestion: which external systems can log in, and with what?\nRead: inputs\u002Fmetadata-production\u002Fconnected-apps\u002F, inputs\u002Fwiki\u002Fintegrations.md\nOut of scope: code quality, the data model\nFinding: claim, evidence (file and line), confidence, severity\nDone when: every connection in the metadata is listed, or marked unknown\n","md",[109,194,195,201,207,213,218,223,228],{"__ignoreMap":107},[112,196,197],{"class":114,"line":115},[112,198,200],{"class":199},"sq-ep","# Brief: connections and logins\n",[112,202,203],{"class":114,"line":121},[112,204,206],{"emptyLinePlaceholder":205},true,"\n",[112,208,209],{"class":114,"line":127},[112,210,212],{"class":211},"sVt8B","Question: which external systems can log in, and with what?\n",[112,214,215],{"class":114,"line":133},[112,216,217],{"class":211},"Read: inputs\u002Fmetadata-production\u002Fconnected-apps\u002F, inputs\u002Fwiki\u002Fintegrations.md\n",[112,219,220],{"class":114,"line":139},[112,221,222],{"class":211},"Out of scope: code quality, the data model\n",[112,224,225],{"class":114,"line":145},[112,226,227],{"class":211},"Finding: claim, evidence (file and line), confidence, severity\n",[112,229,230],{"class":114,"line":151},[112,231,232],{"class":211},"Done when: every connection in the metadata is listed, or marked unknown\n",[57,234,235],{},"Without a brief, the agent summarizes everything it can see. Findings come back without evidence, and the same issue turns up in three areas with three different severities. The brief is also what a reviewer checks the findings against.",[57,237,238],{},"Typical areas are a system overview, connections and logins, code quality and tests, a personal data map, a ticket review and the operations around the system.",[64,240,242],{"id":241},"findings-with-evidence-and-a-register-for-what-you-inherit","Findings with evidence, and a register for what you inherit",[57,244,245],{},"Every finding has four parts: the claim, the evidence as a file and line, a confidence level and a severity. A finding with no evidence doesn't go in.",[57,247,248],{},"Some things aren't findings about the system's design. They're problems the next owner inherits: an expired certificate, a job that fails every night, an integration nobody can explain. They go in a separate register, so the report stays about decisions and the register becomes a work list.",[57,250,251],{},"Before anything reaches the report, check it:",[69,253,254,257,260,263],{},[72,255,256],{},"Drop every finding that has no file to point at.",[72,258,259],{},"Check a sample by hand, and check more of the high-severity ones.",[72,261,262],{},"Treat documentation and meeting notes as claims. Test them against tickets and metadata, and record where they disagree. The disagreement is often a finding of its own.",[72,264,265],{},"Make sure every finding names its environment.",[64,267,269],{"id":268},"layer-the-report","Layer the report",[57,271,272],{},"Different readers need different depths, so the report has three layers:",[69,274,275,278,281],{},[72,276,277],{},"One page: the situation, the main risks and the recommendation.",[72,279,280],{},"Three pages: the options, with timing and commercial shape for each.",[72,282,283],{},"An internal memo for your own team: what you would decide and why, kept apart from the version anyone else reads.",[57,285,286],{},"Every claim in every layer links to a finding, and every finding to a file. A reader who doubts a sentence on the one-pager can follow it down to the export it came from.",[64,288,290],{"id":289},"hand-back-and-clean-up","Hand back and clean up",[57,292,293],{},"When the audit closes, the inputs folder is the most complete copy of the system that exists outside the system itself. Delete it or hand it back to the owner, and say which in the report. Keep the registers. They're the part the team that takes over will use.",[295,296,297],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sq-ep, html code.shiki .sq-ep{--shiki-default:#005CC5;--shiki-default-font-weight:bold;--shiki-dark:#79B8FF;--shiki-dark-font-weight:bold}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":107,"searchDepth":121,"depth":121,"links":299},[300,301,302,303,304,305,306],{"id":66,"depth":121,"text":67},{"id":86,"depth":121,"text":87},{"id":96,"depth":121,"text":97},{"id":183,"depth":121,"text":184},{"id":241,"depth":121,"text":242},{"id":268,"depth":121,"text":269},{"id":289,"depth":121,"text":290},"2026-10-30","A setup for a team auditing a system they didn't build. People run the exports, the agent only reads files, every finding points at evidence, and the report comes in layers.",false,[311,312,313],"How to","> audit a system with an AI agent","that never gets the keys",{},"\u002Fwriting\u002Fhow-to-audit-a-system-with-an-ai-agent-that-never-gets-the-keys",{"title":52,"description":308},"writing\u002Fhow-to-audit-a-system-with-an-ai-agent-that-never-gets-the-keys","Security","5R2RAo0Wh_3TERnqRqs2aZ3Dl6Vzts6UIWyY4YQ9zJk",[321,322,326,329],{"path":315,"title":52,"topic":318},{"path":323,"title":324,"topic":325},"\u002Fwriting\u002Fyour-token-cache-key-has-to-know-about-your-deployments","Your token cache key has to know about your deployments","Backend",{"path":327,"title":328,"topic":318},"\u002Fwriting\u002Fan-install-policy-for-the-year-of-npm-worms","An install policy for the year of npm worms",{"path":330,"title":331,"topic":332},"\u002Fwriting\u002Fwhy-our-purchase-event-waited-for-the-page-to-load","Why our purchase event waited for the page to load","Analytics",1793359540663]