Integrating AI Coding Tools with Repositories
An AI coding tool is a program that reads your repository, sends some of it to a model, and proposes or makes changes. For a platform team, the interesting questions are not about how good the suggestions are. They are about what the tool can read, what leaves the machine, what the tool is allowed to do, how long copies of the data persist, and how an investigator would reconstruct what happened afterward. This article builds a way to answer those questions for a tool that connects to enterprise repositories: how to scope repository access, configure the tool so the configuration itself is controlled, define and test context boundaries, map data retention, and combine approved environments with audit.
The article assumes general computer science and programming knowledge and some familiarity with Git hosting. It builds on two earlier articles in this series. Software Supply Chain Controls in Delivery Pipelines covered dependency and image provenance, signing and verification, privileged build actions, and exception handling. Here, the carried-over idea is that a change reaching a protected branch is only as trustworthy as the identity and process that produced it, and that privileged automation needs explicit limits. Service Objectives, Alerting, and Error Budgets in Practice covered SLI measurement, user-visible failures, burn rates, and release tradeoffs. The idea carried over there is that a control you operate, such as a policy gateway or an audit pipeline, is a service with its own objectives and failure modes. The explanations below do not depend on re-reading either article.
Product documentation changes quickly. The behaviors cited here came from the vendors’ public documentation as read while writing, and they are examples of design choices, not a ranking or an endorsement. Before you rely on any specific setting, read the current documentation for the tool and plan you use, and test the behavior in a non-production repository. The code in this article is an illustrative model written for this article; it uses synthetic files and placeholder values only.
The tool as a system with boundaries
It helps to stop thinking of the tool as a text box and draw its data flow. A typical setup has these parts:
- A client, either an editor extension, a terminal program, or a hosted agent, that runs on a developer machine or in a managed environment.
- A context assembler inside the client that decides which files, snippets, git state, and earlier conversation go into each request.
- A model provider reached over the network, which processes the request and may retain it under terms that depend on the plan.
- Tools the model can call through the client: reading files, running shell commands, editing files, searching the web, and calling external servers.
- Outputs: suggested text, edited files, commits, and pull requests that flow into your normal pipeline.
Each arrow between those parts is a trust boundary: the receiving side must decide what it accepts and what it will do. The table lists where data or authority crosses, and which control lives at that point.
| Boundary | What crosses | Typical control point |
|---|---|---|
| Repository host to client | Files, history, issue and pull request text | The identity and token the client uses; repository permissions |
| Client to model provider | Prompts, selected files, tool results, conversation | Context filtering; provider contract and retention terms; network egress rules |
| Model to tools | Proposed actions: read, write, execute, fetch | Permission rules; approval prompts; OS-level sandbox |
| Tools to external servers | Arguments and results of extension calls | Allowlist of servers; review of what each tool can do |
| Client to local disk | Transcripts, caches, logs | Retention settings; disk controls |
| Output to repository | Commits, branches, pull requests | Branch protection; review requirements; the pipeline from the supply-chain article |
Three risk categories from the OWASP Top 10 for LLM Applications (2025 edition) map directly onto this table. Sensitive information disclosure concerns confidential data reaching outputs or the provider; OWASP’s mitigations include least-privilege access and limiting the model’s connections to data sources. Excessive agency names three causes, which are too much functionality, too many permissions, and too much autonomy, and recommends minimizing extensions, using granular permissions, and requiring human approval for high-impact actions. Prompt injection, covered below, is the way an attacker supplies instructions through content the tool reads. These are risk descriptions from a community project, not a compliance standard, and they do not give you a complete control set. They are useful because they point at the same boundaries as the table.
Repository access
The first decision is whose authority the tool runs with. There are three common patterns, and they have different consequences.
- The developer’s own identity. The tool uses the developer’s credentials, so it can read and write anything the developer can. It inherits every permission the developer has accumulated, including access to repositories unrelated to the current task. This is convenient and gives the tool the largest blast radius per session.
- A shared service account. One credential serves many developers. It is easy to scope, but audit entries show the service account instead of the person, and a leaked token affects everyone who uses it.
- A scoped, short-lived installation credential. GitHub documents that an installation access token for a GitHub App lasts one hour and can be restricted at creation to specific repositories (up to 500) and to a subset of the permissions the app was granted. The token cannot exceed what the installation was granted. A broker that mints such a token per task, for the repositories that task needs, gives you narrow scope and a short lifetime, at the cost of building and operating the broker.
Least privilege means more than the choice of pattern. It means separating the operations. Reading source for context, writing a branch, opening a pull request, and merging are different permissions, and a coding tool rarely needs the last one. A useful default is read access to the repository the developer is working in, write access only to branches the tool creates, and no permission to change protected branches, repository settings, or secrets. Server-side controls such as branch protection and required reviews matter because they do not depend on the client behaving. They are the backstop that holds even if the tool misbehaves or a prompt injection succeeds.
What read access actually exposes
Read access to a repository is wider than the file you have open. The tool may reach:
- Other files in the working tree, including ones that were never meant to be shared: environment files, local config, generated credentials.
- Git history, where a file deleted long ago may still contain a credential.
- Submodules and vendored code with different owners and licenses.
- Symbolic links that point outside the intended directory.
- Text that other people wrote: issue descriptions, pull request comments, and README files, any of which can contain instructions aimed at the tool.
Treat the last item as untrusted input. A tool that reads a comment saying to run a command or send a file is reading data, not receiving orders. Prompt injection is the attempt to make a model follow instructions that conflict with its intended task, and indirect injection arrives through content the application reads. The defense cannot rest on the model recognizing every attack. It rests on the permission layer: the content cannot grant the tool authority that the deployment did not already give it.
Secrets in the repository
Secrets management practice says configuration references a secret and does not contain one, but real repositories contain secrets anyway. GitHub’s push protection blocks pushes that contain recognized credential patterns, and it works for command-line pushes, web commits, API requests, and the GitHub MCP server. Its documentation states limits that matter here: it detects supported patterns (and custom patterns an organization defines), repository-level protection is off by default, and anyone with write access can bypass it by giving a reason. So push protection reduces secrets entering the repository; it does not guarantee that none are present for a tool to read, and it does not act on what a tool sends to a model. The context boundary in the next sections has to handle secrets that are already there.
Tool configuration
A tool’s configuration decides what the tool may do, so configuration needs the same care as any other policy input. Three properties matter: who can set it, which source wins when sources disagree, and whether it can be verified.
Scopes and precedence
Claude Code’s documentation gives a concrete layering that is typical of the category. Settings come from managed settings (deployed by the organization), the command line, a project-local file, a shared project file checked into the repository, and a user file. A key set at a higher level overrides lower levels, and for permission rules the evaluation order is deny, then ask, then allow, with the first match deciding. Deny rules from any scope take precedence over allow rules from any other scope, so a user-level allow cannot override a project-level deny. An organization can also make managed settings the only source of permission rules by setting allowManagedPermissionRulesOnly.
The shared project file is the design choice to examine. Because it lives in the repository, anyone who can merge to the repository can change what the tool is allowed to do for everyone who clones it. That is useful for teams who want consistent rules, and it also means a change to a settings file is a change to a security control. Handle it as you would a pipeline definition from the supply-chain article: put it under review requirements, assign owners for the configuration paths, and do not let the tool itself approve changes to its own configuration.
The rule syntax in this tool is path-based. A deny rule for reading ./.env or ./secrets/** is documented as the way to stop the tool’s file tools from reading those paths. The structure of such a configuration, following the documented shape and not executed against the tool for this article, looks like this:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./secrets/**)"
]
}
}
Three details from the documentation show why testing matters more than reading the rules. First, a Read deny rule is applied on a best-effort basis to the tools that read files, such as search and mention features, and the documentation states that the rule does not apply to a command that reads files without naming them (its example is a recursive grep run from the directory holding the file) or to arbitrary subprocesses such as a script that opens files itself. Second, a path rule written for a tool the permission check does not consult for paths, such as Write or Glob instead of Edit or Read, is accepted but never consulted, and the tool warns at startup. Third, rule patterns anchor to a location: a deny rule written in user settings with a single leading slash is anchored to the user’s configuration directory, not to each project, so a rule meant for every project needs an absolute or home-relative path. Each of these is a case where a rule can look correct and fail to do what its author intended.
Configuration you can verify
For each control, write a check that fails when the control does not work. Examples:
- A fixture repository containing a placeholder
.env, asecrets/directory, and a symlink to the placeholder. A scripted session asks the tool to read each and records whether the read was refused. - A check that the effective configuration on a developer machine contains the managed rules, for instance by reading the tool’s reported settings and comparing a hash of the rule set with the approved version.
- A check run in continuous integration that fails when the checked-in tool configuration changes without an approving review from the configuration’s owners.
The policy as code approach fits: the approved configuration is a versioned artifact, and enforcement points compare against it. The checks do not prove that an adversary cannot get around a control. They show that the control works for the cases you thought of and make regressions visible when the tool changes its behavior in an upgrade.
Extensions and external servers
The Model Context Protocol (MCP) is a way for a client to connect a model to external tools and data. Its specification states that tools represent arbitrary code execution and must be treated with caution, that descriptions of tool behavior should be considered untrusted unless they come from a trusted server, and that hosts must obtain user consent before invoking a tool. It also says the protocol itself cannot enforce these principles; implementors have to build consent and access controls. For a platform team this translates into an allowlist of approved servers, review of each server’s code and permissions the way you review a dependency, and a rule that a server’s own claims about what its tools do do not substitute for your review.
The Claude Code sandbox documentation adds a detail that is easy to miss: the sandbox covers shell commands and the processes they start, while the built-in file tools, local MCP servers, and hooks run outside it. A sandbox setting that restricts file reads for shell commands does not constrain a local MCP server that reads those files. Whatever your tool, ask for each extension point which enforcement mechanism covers it.
Version and rollout
Tools update often and change defaults. Pin approved versions or channels where the tool allows it, roll out a new version to a pilot group first, and rerun the verification checks before widening the rollout. The check results are the evidence that an upgrade did not silently weaken a boundary.
Context boundaries
Context is whatever the tool assembles and sends to the model for a request. Context window limits mean the tool cannot send everything, so a context assembler selects. That selection is the first place to put a boundary, because content that is never selected cannot leak through that request. Being included in the context does not mean the model uses it correctly, but anything included has left your machine, so the boundary is about exposure first.
Kinds of boundary
- Path-based exclusion. Specific files and directories are never read or sent. GitHub Copilot’s content exclusion works this way: repository admins and organization owners (and enterprise owners) list paths using fnmatch-style patterns that are case insensitive, for example
**/.envfor all environment files, and the documentation says changes can take up to 30 minutes to reach IDEs. - Content-based filtering. Content matching a pattern, such as a credential format, is blocked or redacted, regardless of its path.
- Scope-based limits. The tool is restricted to a repository, a directory, or a set of repositories selected for the task.
- Classification-based limits. Repositories carry a data classification and the tool’s availability depends on it. The tool is not offered at all for the most sensitive repositories.
- Return-path limits. Tool output, such as the result of a command or a fetched page, also enters the context. If it returns to the model, a later proposed action must be authorized again, because the output may contain text written by someone untrusted.
Documented limits of path exclusion
Path exclusion is the most common control and the one with the most surprising gaps. GitHub documents that content exclusion does not apply to symbolic links or to repositories on remote filesystems, that Copilot can still use semantic information such as type information and hover definitions for symbols and general project properties like build configuration, and that some surfaces are not covered: the Copilot CLI and agent mode in IDE chat do not support it, and chat or agent support is missing in some editors. The Claude Code documentation states the Read rule limits above and gives a design response: for operating-system level enforcement that blocks all processes from accessing a path, enable the sandbox, with its own coverage limits described earlier.
These are descriptions of two vendors’ documented behavior at the time of reading, and they will change. Their value here is the pattern. Every exclusion mechanism has a coverage list, and the controls you rely on are the ones whose coverage you have tested for your configuration. The table summarizes the pattern.
| Control | What it stops | What it does not stop |
|---|---|---|
| Path exclusion in the tool | Named files entering context on covered surfaces | Uncovered surfaces; symlinks (in the documented Copilot case); indirect information such as symbol types |
| Read deny rules in the tool | Built-in file tools reading matched paths | Commands that read files without naming them; subprocesses that open files themselves |
| OS-level sandbox | Shell commands and child processes reaching restricted files and hosts | Built-in file tools, local extension servers and hooks, which run outside it |
| Push protection | Pushing recognized credential patterns | Unrecognized formats; bypass by users with write access; secrets already in history |
| Network egress allowlist | Traffic from the environment to unapproved hosts | Content sent to approved hosts, including the model provider |
| Server-side branch rules | Merging unreviewed changes | Anything that happens before a change is proposed |
No single row is enough. The combination covers more, and the gaps shift with configuration, so you test the combination.
A model of a context gateway
To make the idea concrete, consider a small model of a gateway that decides which files in a repository may enter an AI tool’s context. It is not a product and not the mechanism of any named tool. It was written for this article to show the decisions a boundary has to make. The model has three checks. A path check matches the requested path against deny patterns (**/.env, **/.env.*, secrets/**, **/*.pem, infra/prod/**) using case-insensitive fnmatch-style matching; because Python’s fnmatch has no ** semantics, the code treats **/ as any prefix, including none. The same check is repeated on the resolved path after following symbolic links. A content check then rejects files containing text shaped like an AWS access key identifier or a private key header. Every decision is logged with the rule that decided it, and allowed files are logged by size and a truncated hash, not by content. The script builds a temporary repository of synthetic files with placeholder values only, runs the gateway on each file, and prints the log. The listing is complete; the decisions follow from the order of the checks.
# Illustrative context gateway. All paths, patterns, and "secrets" are synthetic.
import fnmatch, hashlib, json, os, re, tempfile, time
DENY = ["**/.env", "**/.env.*", "secrets/**", "**/*.pem", "infra/prod/**"] # path policy (assumed)
SECRET_PATTERNS = {"aws_access_key_id": re.compile(r"AKIA[0-9A-Z]{16}"),
"private_key_block": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")}
def denied(rel):
rel = rel.replace(os.sep, "/")
for pat in DENY:
# fnmatch has no '**' semantics, so treat '**/' as 'any prefix, including none'
variants = {pat, pat.replace("**/", "")}
if any(fnmatch.fnmatchcase(rel.lower(), v.lower()) for v in variants):
return pat
return None
def decide(root, rel, audit):
requested = rel
resolved = os.path.relpath(os.path.realpath(os.path.join(root, rel)), os.path.realpath(root))
rule = denied(requested)
via = "requested"
if rule is None and resolved != requested:
rule, via = denied(resolved), "resolved"
if rule:
audit.append({"path": requested, "resolved": resolved, "decision": "deny", "rule": rule, "matched_on": via})
return None
data = open(os.path.join(root, rel), "rb").read()
hits = [n for n, p in SECRET_PATTERNS.items() if p.search(data.decode("utf-8", "replace"))]
if hits:
audit.append({"path": requested, "decision": "deny", "rule": "secret-pattern:" + ",".join(hits)})
return None
audit.append({"path": requested, "decision": "allow", "bytes": len(data),
"sha256_12": hashlib.sha256(data).hexdigest()[:12]})
return data.decode()
def make_repo():
root = tempfile.mkdtemp(prefix="repo_")
files = {
"src/app.py": "def handler(event):\n return {'ok': True}\n",
"src/config.py": "API_KEY = 'AKIAABCDEFGHIJKLMNOP' # synthetic pattern-shaped value\n",
"src/token.py": "TOKEN = 'zk_live_9f8e7d6c5b4a39281706f5e4d3c2b1a0' # synthetic, no known prefix pattern\n",
".env": "DB_PASSWORD=placeholder-not-a-real-password\n",
"secrets/db.txt": "placeholder\n",
"infra/prod/main.tf": "# prod infra\n",
"docs/README.md": "# Docs\n",
}
for rel, content in files.items():
p = os.path.join(root, rel); os.makedirs(os.path.dirname(p), exist_ok=True)
open(p, "w").write(content)
os.symlink(os.path.join(root, ".env"), os.path.join(root, "docs", "env-link.txt"))
return root, sorted(list(files) + ["docs/env-link.txt"])
if __name__ == "__main__":
root, rels = make_repo(); audit = []
allowed = {r: decide(root, r, audit) for r in rels}
for e in audit: print(json.dumps(e))
print("\nin context:", [r for r, c in allowed.items() if c is not None])
# What if the policy only checked the requested path?
naive = [r for r in rels if denied(r) is None]
print("naive path-only check would admit:", [r for r in naive if r not in [e['path'] for e in audit if e['decision']=='allow']])
The script prints one JSON log line per file, then the files that entered the context, then the files that a policy checking only the requested path, with no content check, would additionally have admitted.
{"path": ".env", "resolved": ".env", "decision": "deny", "rule": "**/.env", "matched_on": "requested"}
{"path": "docs/README.md", "decision": "allow", "bytes": 7, "sha256_12": "b8c2322c9fb6"}
{"path": "docs/env-link.txt", "resolved": ".env", "decision": "deny", "rule": "**/.env", "matched_on": "resolved"}
{"path": "infra/prod/main.tf", "resolved": "infra/prod/main.tf", "decision": "deny", "rule": "infra/prod/**", "matched_on": "requested"}
{"path": "secrets/db.txt", "resolved": "secrets/db.txt", "decision": "deny", "rule": "secrets/**", "matched_on": "requested"}
{"path": "src/app.py", "decision": "allow", "bytes": 44, "sha256_12": "36ace623dc77"}
{"path": "src/config.py", "decision": "deny", "rule": "secret-pattern:aws_access_key_id"}
{"path": "src/token.py", "decision": "allow", "bytes": 89, "sha256_12": "61c792aa233b"}
in context: ['docs/README.md', 'src/app.py', 'src/token.py']
naive path-only check would admit: ['docs/env-link.txt', 'src/config.py']
The table restates the log by file.
| File in the synthetic repository | Decision | Deciding rule |
|---|---|---|
.env | deny | path **/.env, matched on the requested path |
docs/README.md | allow | none; logged as 7 bytes with a hash prefix |
docs/env-link.txt (symlink to .env) | deny | path **/.env, matched on the resolved path |
infra/prod/main.tf | deny | path infra/prod/** |
secrets/db.txt | deny | path secrets/** |
src/app.py | allow | none; logged as 44 bytes with a hash prefix |
src/config.py (placeholder shaped like an access key ID) | deny | content pattern for access key identifiers |
src/token.py (placeholder token in an unrecognized format) | allow | none; no pattern matches |
Only docs/README.md, src/app.py, and src/token.py entered the model’s context. A check on the requested path alone, with no content check, would additionally have admitted docs/env-link.txt and src/config.py.
How to read the results:
.env,secrets/db.txt, andinfra/prod/main.tfare denied by path rules, and each denial records which rule matched.docs/env-link.txtis a symlink whose requested path looks harmless. The gateway also checks the resolved path, finds.env, and denies it. A check on the requested path alone would have admitted it, which is the last line of the output. This mirrors the symlink gap in the documented Copilot behavior and the resolved-path design in the Claude Code documentation: whichever tool you use, test links explicitly.src/config.pyhas no excluded path, but its content matches a credential pattern, so the content filter denies it. The path-only check would have admitted it too.src/token.pyis allowed. Its placeholder token has a format that none of the patterns recognizes. This is the limit of pattern-based detection, and it is the reason that the content filter is a safety net and not an inventory of secrets. A credential in an unknown format passes. Reduce that risk by keeping secrets out of source in the first place, and by adding patterns for the formats your organization issues.- Allowed files are logged by byte count and a truncated hash, not by content. An audit entry that stored the file would itself become another copy of the sensitive content.
The decision log is the second output of the gateway. For each decision it says what was requested, what was decided, and which rule decided. That record is what lets an investigator answer why a file did or did not reach the tool.
What the model does not show: it runs on one machine over a handful of files. It does not model case-sensitivity differences between file systems, Unicode normalization of paths, hard links, files that change between the check and the read, or content split across files. Real enforcement needs to handle those, and it needs to run where the tool cannot bypass it.
Where a gateway can and cannot sit
A gateway like this works only if the tool’s requests pass through it. If the tool reads files directly from disk, the gateway must be implemented as the tool’s own configuration or as an operating-system control. If the tool talks to the model provider through a proxy you operate, a proxy can inspect and filter outgoing content, with the limits that the proxy sees only what the tool sends and that inspection of encrypted or encoded content is difficult. The right placement depends on what the tool supports. Choosing a tool because it supports enforcement points you need is more reliable than adding enforcement afterward.
Data retention
Retention questions have to be asked about every copy of the data, not only the one at the model provider. A single interaction can leave copies in several places, each with its own lifetime and owner.
Data retention means deciding why information is kept, for how long, and what happens at the end. A duration needs a starting event and an expiry action, and derived copies need lifecycle metadata of their own. For an AI coding tool, build an inventory of copies:
| Copy | Where it lives | Who controls its lifetime | What to determine |
|---|---|---|---|
| Local session transcript | Developer machine | The client’s cleanup settings and the developer | Location, whether encrypted at rest, default period, how to shorten it |
| Request and response at the provider | Provider systems | Provider terms and your plan | Retention period, whether it is used for training, whether zero-retention terms are available and enabled |
| Telemetry and error reports | Provider or your collector | Client settings and your collector configuration | What fields are included, whether prompts or file paths are in them, how to disable or redact |
| Feedback or bug reports submitted by users | Provider | Provider terms | Whether users can send transcripts, how long they are kept, how to disable the command |
| Your own audit records | Your log platform | Your retention schedule | Whether they contain prompts or file contents |
| Output artifacts | Repository, pull requests, CI logs | Your repository and CI retention | Whether generated text includes pasted sensitive data |
The Claude Code data usage documentation illustrates how many separate answers one tool can have. It states that under commercial terms (Team, Enterprise, and API) Anthropic does not train generative models on code or prompts sent to Claude Code unless the customer chooses to provide data, such as through the Development Partner Program. It states a standard 30-day retention period for commercial users, with zero data retention available to qualified accounts on a per-organization basis, and different consumer-plan retention depending on a data-use setting. It says the client stores session transcripts locally in plaintext for 30 days by default, adjustable with cleanupPeriodDays. It says transcripts submitted through the feedback commands are retained for five years, and that the feedback command can be disabled with an environment variable. Telemetry has separate switches. These are the documented values at the time of reading; they depend on plan and configuration, and they change.
Two points follow from this list. First, a statement like “the provider doesn’t train on our data” and a statement like “the provider doesn’t retain our data” are different claims with different evidence. Keep them separate when you write policy and when you answer a reviewer. Second, the longest-lived copy may be one created by a convenience feature, such as a feedback command that attaches a transcript, so inventory the commands and not only the main data path.
This article does not state retention periods for GitHub Copilot. Confirm the terms for your plan from the current GitHub documentation before you rely on them.
Retention for your own records
The audit log you build can become another sensitive copy. Claude Code’s OpenTelemetry export, for example, redacts prompt text, assistant response text, tool parameters, and tool content by default, and each is enabled with a separate setting; enabling raw API body logging implies consent to all of them. Turning those on would put prompts and file contents into your log platform, where retention and access rules must match the sensitivity. Decide per field what the investigation needs. Often a request identifier, a path, a decision, and a hash are enough to establish what happened and let you retrieve the content from its source if needed.
What this section cannot decide
Retention periods interact with contractual, regulatory, and legal-hold obligations that depend on your organization and jurisdiction. This article cannot tell you which apply. The technical task is to produce the inventory and the evidence of settings; legal and compliance owners decide which settings are acceptable.
Approved environments
An approved environment is a defined combination of tool version, configuration, identity, model provider arrangement, and network path in which developers may use the tool on enterprise repositories. The definition matters because the controls above only hold when those parts are known.
A workable definition includes:
- The tool and version or channel, with the configuration source (managed settings, not user-editable).
- The identity model, for example per-task scoped credentials or the developer’s own identity with a named maximum scope.
- The provider arrangement, including plan, retention terms, and any zero-retention agreement.
- The execution environment, such as a managed workstation, a development container, or a remote managed environment. A container or virtual machine gives you an isolation boundary you control, while an OS-level sandbox for shell commands applies narrower limits. The Claude Code documentation notes that its sandbox runs on macOS, Linux, and WSL2 and that on native Windows commands run unsandboxed, so the platform is part of the definition.
- The network path, with an egress allowlist that includes the approved provider endpoints and excludes others.
- Repository eligibility, tied to classification: which repositories the approved environments may open, and which are excluded entirely.
Enforcement and its limits
Enforcement has two sides. You can make the approved path the easiest one by deploying managed configuration, providing the container image, and making the approved credentials available. You can also constrain the alternatives with egress rules and repository permissions. You cannot reliably detect every unapproved tool. A developer can paste code into a web page on a personal device, and nothing in the repository host or the corporate network may show it. Policy for those cases is about rules, training, and legal agreements. This article does not cover that, and a technical control that claims to detect all of it should be viewed with suspicion.
Audit
An audit log answers questions. Write the questions first, and then determine which source can answer each:
| Question an investigator will ask | Candidate source | Gap to check |
|---|---|---|
| Which repositories did this identity access during the session? | Repository host audit log, token issuance log | Whether the tool used the developer’s identity, hiding which actions the tool took |
| Which files entered the model’s context? | Gateway or client decision records | Whether the client emits them; whether they log hashes without content |
| Which commands did the tool run, and were they approved? | Client telemetry of tool decisions and results | Whether command details are enabled; whether the client can be altered by its user |
| Which configuration was in force? | Configuration version recorded at session start | Whether the client reports it |
| What was committed or opened as a pull request? | Repository host | Attribution of commits the tool made |
| Was anything sent to an unapproved destination? | Network egress logs | Encrypted traffic to approved hosts |
Claude Code’s OpenTelemetry export is one example of client-side evidence. It documents events for user prompts, tool permission decisions (accept or reject), tool results, API requests, MCP server connections, and permission-mode changes, plus metrics such as sessions, tokens, cost, and commits. Attributes include session and user identifiers, and the documentation notes controls for attributes that raise metric cardinality, such as session identifiers. The documented default is to redact prompt content and tool details. The practical lesson is general. Client-side events are rich but are produced by software on the developer’s machine. Use them for investigation and trend analysis, and corroborate important claims with server-side records the client cannot edit, such as the repository host’s audit log and the egress logs.
Treat the control plane as a service
The prerequisite on service objectives gives a way to think about the failure modes of your own controls. Define indicators for them:
- Policy decision availability. The fraction of tool requests that received a decision from the gateway within a latency bound.
- Audit completeness. The fraction of sessions with audit events from start to end. A drop means a blind spot.
- Configuration conformance. The fraction of active clients whose effective configuration matches the approved version.
- Exception age. How long each approved exception has stayed open past its review date.
Each indicator needs an explicit target, a measurement window, and a rule for missing data. The decision that deserves the most thought is what happens when the control fails. A gateway that fails closed blocks developers when it is down, which converts a security outage into a productivity outage and creates pressure to bypass it. A gateway that fails open keeps developers working and loses protection during the outage. Neither is universally right. For the most sensitive repositories you may choose to fail closed; elsewhere you may choose to fail open with an alert and a bounded budget of degraded time, using the same budget logic as an error budget. The point is to make the choice explicitly, per tier, before an outage forces it.
Exceptions
Teams will ask to use the tool outside the approved environment, or to open a repository that is excluded. Treat each as a recorded exception with an owner, a scope, a reason, an expiry date, and compensating controls, in the way the supply-chain article treated exception handling. An exception without an expiry becomes policy by accident.
Putting a rollout together
An order that produces evidence at each step:
- Inventory the data flow for the candidate tool using the boundary table, and list every place data is stored, with owner and lifetime.
- Define repository tiers by sensitivity, and decide which tiers allow the tool and in which environments.
- Choose the identity model, and implement scoped credentials, or accept the developer identity with a stated maximum scope and server-side backstops.
- Write the managed configuration and put it under review. Include permission rules, extension allowlists, telemetry settings, and retention settings.
- Build the verification fixtures: placeholder secret files, symlinks, search commands that do not name files, and extension calls. Run them against each approved version and record the results.
- Define the audit sources from the question table, and test that an investigator can reconstruct a scripted session end to end.
- Pilot with one team, measure the indicators above, and review exceptions and failures before widening.
- Repeat the checks on every tool upgrade and on every configuration change.
Limits of this approach
The controls reduce exposure; they do not make an AI coding tool safe in an absolute sense. Pattern-based filters miss unknown formats, as the model showed. Path rules miss indirect information. A sandbox covers part of the tool. Client-side logs depend on the client. Retention terms depend on contracts and plans. And none of this addresses the quality of the code the tool produces or the license status of generated output; the next article in the series, Testing and Reviewing AI-Generated Code in CI, takes up verification of the generated changes that flow back into your pipeline. Treat the numbers and outputs in this article as results from a synthetic model, and treat the vendor behaviors as time-stamped examples that you must re-verify for your own tools.
References
- OWASP GenAI Security Project: 2025 Top 10 for LLM Applications
- OWASP: LLM06:2025 Excessive Agency
- OWASP: LLM02:2025 Sensitive Information Disclosure
- GitHub Docs: Content exclusion for GitHub Copilot
- GitHub Docs: Excluding content from GitHub Copilot
- GitHub Docs: Generating an installation access token for a GitHub App
- GitHub Docs: About push protection
- Claude Code documentation: Settings files and precedence
- Claude Code documentation: Configure permissions
- Claude Code documentation: Configure the sandboxed Bash tool
- Claude Code documentation: Data usage
- Claude Code documentation: Monitoring usage with OpenTelemetry
- Model Context Protocol specification (2025-06-18)
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
