DEV Community

Cover image for I Stopped Trusting My AI Coding Agent. So I Touched some grass & Built something to Watch It
Ayush Ahire
Ayush Ahire

Posted on

I Stopped Trusting My AI Coding Agent. So I Touched some grass & Built something to Watch It

The moment that started this

I use Claude Code daily. Mostly it's a cool tool. A few weeks back while refactoring a large & messy bug-bounty codebase, it also touched a config file I never asked it to touch. I only caught it because I happened to see it while pushing the code to github, I saw a file appear in commit history which I never touch.

That's what pissed me, not the mistake, the lack of visibility. In a codebase that size, I can't manually catch every unintended write, every file it peeked into it didn't need. So instead of asking "is this agent helpful," I started asking a security question: what is it actually doing, and who's watching?

What I wanted

  • What has it read, right now, this session?
  • Did anything it read contain something sensitive like a secret, PII, an internal marker?
  • Did that data go anywhere it shouldn't like an external API, outside the project?
  • If it touches something off-limits — .env, an admin module do I get alerted before it happens, not a diff after?

The core: a policy engine, not a vibe check (I'm there for Vibe Check)

I built this on a toy agent ( Like my ex used me for fun) first a minimal loop with four tools (read_file, write_file, run_shell, call_api), each one gated through a policy layer before it's allowed to execute. Two jobs: tag sensitive data the moment the agent touches it, and block/pause anything that tries to carry tagged data across a trust boundary.

lets see some codes now

# policy.py -> the actual detection logic, unedited

SECRET_RE = re.compile(
    r"(sk-[a-zA-Z0-9]{16,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z\-_]{35}|"
    r"ghp_[a-zA-Z0-9]{20,}|Bearer\s+[a-zA-Z0-9\-_\.]{20,}|"
    r"[a-fA-F0-9]{32,}|-----BEGIN [A-Z ]+PRIVATE KEY-----)"
)

def evaluate_call(tool_name, target, params, inherited_tags):
    if tool_name == "run_shell" and DANGEROUS_SHELL_RE.search(target):
        return PolicyResult(Decision.BLOCK, 100, ["dangerous_shell_pattern"])

    if Tag.SECRET in inherited_tags and tool_name in ("call_api", "write_file") and _is_external(target):
        return PolicyResult(Decision.BLOCK, 95, ["secret_data_exfil_attempt"])

    if Tag.PII in inherited_tags and tool_name in ("call_api", "write_file") and _is_external(target):
        return PolicyResult(Decision.PENDING_CONFIRM, 70, ["pii_crossing_trust_boundary"])

    return PolicyResult(Decision.ALLOW, 0, [])
Enter fullscreen mode Exit fullscreen mode

The taint tags carry forward across steps if step 1 reads a file with PII in it, step 3 still knows that, even though it's a completely different tool call. That's the part that matters: not "was this one action dangerous," but "did sensitive data quietly travel somewhere it shouldn't have, across a chain of individually-innocent steps."

Here's it actually running, unedited output from my terminal:

>>> classify_content("Contact me at john@example.com for details")
['pii']

>>> evaluate_call("run_shell", "rm -rf /", {}, set())
block ['dangerous_shell_pattern']

>>> evaluate_call("call_api", "https://evil-domain.com/exfil", {}, {Tag.SECRET})
block ['secret_data_exfil_attempt']
Enter fullscreen mode Exit fullscreen mode

Where SigNoz comes in (Bro's got some Vibe)

Every decision above gets wrapped in an OpenTelemetry span and shipped to SigNoz, self-hosted:

# instrumentation.py
with tracer.start_as_current_span(f"tool.{tool_name}") as span:
    span.set_attribute("tool.name", tool_name)
    span.set_attribute("tool.target", target)
    span.set_attribute("data.tags", ",".join(taint_ctx.snapshot()))
    span.set_attribute("risk.score", policy_result.risk_score)
    span.set_attribute("policy.decision", policy_result.decision.value)
Enter fullscreen mode Exit fullscreen mode

A log line tells you something happened. A trace tells you the shape of what happened — the whole session as a waterfall, in order, so I can open one session and see exactly what it touched and why. That's the actual reason I picked SigNoz over just writing to a log file:

  • Self-hosted, open source — I'm literally tagging secrets and PII in these spans. I didn't want that flowing into someone else's SaaS.
  • Trace explorer — click into any tool call, see its tags, its risk score, the policy decision, and why.
  • Metrics + traces together — one place to ask both "what happened in this session" and "is blocking trending up as I use this more."

Round two: the real target

A toy agent proves the mechanism. It doesn't prove anything about the tool I actually use daily. So the next step is pointing this at real Claude Code sessions, using its PreToolUse hook which can fire before any tool executes and deny it outright, including over plain HTTP:

// settings.json (draft)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Edit|Write",
        "hooks": [
          { "type": "http", "url": "http://localhost:8080/hooks/pre-tool-use", "timeout": 10 }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

My server on the other end reuses evaluate_call() unchanged, just fed from Claude Code's JSON payload instead of my toy agent's. The policy for the repo I'm working in right now is blunt: don't read .env, don't touch admin/, don't write to config without asking and tell me the instant it tries.

Where I'm at

Toy-agent version: built, tested, working the output above is real. Claude Code integration: in progress. I'm publishing this mid-build because the honest version is more useful than the polished one.

If you've ever wondered what your coding agent actually touched last session you probably don't know either. That's the gap this closes. Touch some grass before your agent touches something that It should not.


Building something similar, or think my regex-based PII detector is going to embarrass me in front of judges? I'd like to hear about it.

Top comments (0)