{"slug": "what-i-learned-by-putting-github-copilot-behind-a-mitm-proxy", "title": "What I learned by putting GitHub Copilot behind a MitM proxy", "summary": "A developer reverse-engineered GitHub Copilot by placing it behind a man-in-the-middle proxy, uncovering details about its network traffic, harness, memory, and how context is becoming the product. The findings highlight that Copilot, like other AI-powered apps built on Electron, shares a common architecture that can be probed to reveal inner workings.", "body_md": "# I put GitHub Copilot Behind a MITM Proxy. Here's What I found.\n\n### A look inside Copilot’s network traffic, harness, memory, and how context is becoming the product.\n\n*Hello, Rafael here - every week I cover interesting challenges and developments that I’ve come across recently through the lens of an engineer building AI systems.*\n\n*Subscribe to get weekly issues 👇*\n\nThere has been a flurry of AI-powered apps and AI features in the last couple of years. Incumbent players like **Slack** have swiftly [added AI features to its roster](https://slack.com/intl/en-gb/help/articles/25076892548883-Guide-to-AI-features-in-Slack). For AI-native ones like **Cursor, Notion, ChatGPT Desktop **and** Claude Desktop, **AI was always part of the *raison d’être. *\n\nThe more AI features these apps released, the more I became inclined to look at their inner workings. Hopefully I would be able to uncover a bit of what’s running under the hood; at the very least, I would learn one thing or two about desktop app development.\n\nCoincidentally, I noticed I started exhausting my Copilot credits earlier and earlier each month. This ended up pulling me towards selecting a main candidate for my experiments. I decided to dive deep into **VS Code and Copilot.**\n\n### One common denominator: Electron\n\nCommon amongst all of the apps above is the fact that they are built using [Electron](https://www.electronjs.org/). Electron is a JavaScript framework which helps developers build and distribute desktop applications. In layman’s terms, it works by bundling a Node.js runtime along with HTML, CSS and JavaScript artifacts, which are then rendered via [Chromium](https://www.chromium.org/Home/).\n\nThis removes the need of having multiple codebases in native languages for different platforms (for instance, C# for Windows and Swift for macOS), making it easier for developers to build desktop applications that run across multiple platforms from a single codebase. (Native modules and certain packaging steps still often require per-platform handling, but the bulk of the application logic is shared.)\n\nBecause they share Electron, they share a rough architecture, which means whatever I learned probing one should transfer to the others.\n\n### Network packets, then source\n\nMy first instinct was to just skim through [VS Code source](https://github.com/microsoft/vscode) and see if it could answer my questions. The problem was that I didn’t have a full set of questions yet - and hunting for them across millions of lines of code would cost me either **too much time** or **too many tokens. **\n\nSource codetells you what an app can do; discovering what it actually does at runtime is more challenging. Especially when you still don’t know what you’re looking for.\n\nThere was a second problem. VS Code is an exception amongst the apps I had started with: its source code (or at least the majority of it) is open. This is not the case for **Claude, ChatGPT, Codex, Notion,** and **Slack**.\n\nThat started pushing me toward the **reverse engineering route**: passively watch the traffic first, let the requests and responses tell me which questions would be worth asking, and only then go to the source to confirm (or disprove) what I was seeing.\n\nIt meant getting my hands dirty with Electron’s architecture and network stack - skills that wouldn’t hurt to have afterwards.\n\n### Electron’s network architecture\n\nBy now we know that Electron apps ship with Chromium. The browser provides the rendering engine for the application’s web based UI, but it also provides a network stack that renderer processes can use for HTTP and WebSocket connections.\n\nThis is a common (and recommended) option for enabling apps to speak to a remote backend, but it is not the only one. Applications can also make HTTP requests using Node’s *http/https/fetch*. Which path the request takes becomes important when you’re trying to intercept it.\n\nIn some cases, like with VS Code, the application will have a decoupled architecture, where there’s a **separate group of processes that acts as an extension host**. This helps maintain **clear boundaries** between distinct responsibilities; in the case of VS Code, a clear boundary between UI, code IDE functionality and plugins/extensions.\n\n### Inspecting network traffic from Electron apps\n\nOne of the classic ways to intercept an application’s network traffic is by standing up a proxy server, and configuring this application to use it.\n\nThe proxy acts as a **man-in-the-middle (MITM)**: it intercepts HTTP requests from a client, forwards them to a server, and relays back the server responses to the client.\n\nFun fact: a similar approach is quite common in corporate network environments for traffic inspection purposes, especially in highly regulated industries. Fittingly, one of the main open source tools used for this is called[mitmproxy], which we will use in the next steps.\n\nAn important detail is that most of the network traffic nowadays happens via secure HTTP (HTTPS). This means traffic is encrypted using TLS.\n\nBy trusting mitmproxy’s locally generated certificate authority (CA), the client can accept the certificates mitmproxy generates on the fly for each destination. Instead of a single end-to-end encrypted connection, you get two: one between the application and mitmproxy, and another between mitmproxy and the destination server.\n\nmitmproxy can therefore decrypt the request, inspect it, establish a separate TLS connection upstream, and forward the response back to the application.\n\n### Getting started\n\nIf you don’t want to follow along with the code and would just like to see the results, feel free to skip this section.\n\n**Installing mitmproxy**\n\nOn macOS, the simplest way is to use brew:\n\n```\nbrew install mitmproxy\n```\n\n#### VS Code Configuration\n\nWe need to change some settings in VS Code to route its traffic via mitmproxy. You can change these settings by using the hotkey combination `Cmd+Shift+P`\n\nand searching for *User Settings. *You will then need to make sure that the settings below have the following values:\n\n**Http Proxy**: http://localhost:8080 (mitmproxy will be listening for connections at this port)** Http Proxy Strict SSL**: unchecked (we want to skip verification of mitmproxy’s certificate against a list of CAs)** Http: Proxy Support**: override (force proxy support for extensions)\n\nAfter making these changes, be sure to restart VS Code.\n\n#### mitm web UI\n\nThe final step before getting started is starting up mitmproxy’s web UI:\n\n```\nmitmweb\n```\n\nGive it a few seconds and you should start seeing some network traffic from VS Code flowing through it.\n\nYou will notice some text fields on top. You can ignore most of them for now; the most useful is the first one, *Search*. This field provides powerful search capabilities like keyword search, regex, etc. For instance, if we are particularly interested in the requests made by VS Code to its Extensions Marketplace API, we can simply use `marketplace`\n\nas a filter string.\n\nThis will match all requests to `https://marketplace.visualstudio.com`\n\nand all its subpaths.\n\n#### Stale Extension Host Processes\n\nIt could be that even after all this dance, your proxy still doesn’t capture extension traffic. This can happen if VS Code’s Extension Host process group becomes stale. To confirm this, run from the terminal:\n\n```\nps -eo pid,ppid,lstart,command | grep -i -E \"copilot|extensionHost|Code Helper\"\n\n# Should display something like this:\n\n27896 27243 Fri Jul 24 15:40:11 2026.    \\\n    /Applications/Visual Studio Code.app/Contents/Frameworks/ # (...)\n```\n\nConfirm the date that is displayed. If it’s not the same date and time from when you restarted VS Code, the extension host process is most likely stale. Solving this is simple:\n\nIn VSCode, open the Command Palette (\n\n`Cmd+Shift+P`\n\n)Run\n\n**“Developer: Restart Extension Host”** Re-run your\n\n`ps`\n\ngrep afterward - you should now see new PIDs with today’s timestamp for`Code Helper`\n\n## What Copilot Does Before You Type Anything\n\nQuickly skim through the network requests from VS Code in mitmweb and you will notice that the majority of them are related to either GitHub or Github Copilot. Before we hit a single key in VS Code or in the Copilot extension, some HTTP requests are made.\n\n### High Level Analysis\n\nRequests made by VS Code and Copilot during the bootstrap stage can be allocated into one of the following categories: *Auth & Session, Config & Policy, MCP Registry, Repo & Session Context, Model Discovery and Recent repos.*\n\nIn the next paragraphs, we discuss what I found out about each of these types of requests: what’s included in headers and payloads for requests and responses.\n\n### Authentication and session bootstrap\n\nThis is the first thing done by Copilot at startup. It fetches an [OAuth](https://en.wikipedia.org/wiki/OAuth) token, exchanges it for a short-lived token, and validates the user’s entitlements. The flow is quite a regular OAuth one; it is described in the diagram below.\n\n### Model and capability discovery\n\nBefore making any LLM requests, Copilot checks which models and agent capabilities are available for your account/plan.\n\nThere are two separate kinds of requests. First, a request is made to `/models`\n\n. This initial request returns a general list of models which are available within Copilot.\n\nThen, a second request is made to `/agents/swe/models`\n\n. This is a specific request to find out which models are available for **agentic capabilities** related to Software Engineering (SWE).\n\n## Some details on prompts, context and harness\n\nPost bootstrap is where things get interesting.\n\n#### Copilot’s model router\n\nI selected *Auto* mode for all the Copilot tests in this experiment. After I sent each message, I was able to capture a request to a `/models/session/intent`\n\nendpoint before any model answered.\n\nWhat’s happening here is: your prompt gets scored against possible intents, such as `code-gen`\n\n, `debugging`\n\n, `reasoning`\n\nand `tool-use`\n\n. The intent classification outcome helps Copilot define which of the available models will fulfill the task.\n\nThis is not really a secret; [such behaviour is described in Copilot’s documentation](https://docs.github.com/en/copilot/concepts/models/auto-model-selection). Still, it was fun to see the actual requests and responses behind it.\n\n### (Secret) environment variables\n\nI started to play around with [inline completions and ghost text](https://docs.github.com/en/copilot/concepts/completions/code-suggestions), watching what was being sent via HTTP. I already knew inline completions inject the current file into prompts as context; that’s how it’s supposed to work. So no surprise here thus far.\n\nBut I still wondered about what else got sent, so I did a small test. I dropped a fake secret into a `.env`\n\nfile - the infamous file all of us kids are told not to commit, but some of us still do.\n\n```\nTEST_ENV_VAR_SECRET=”a realistic looking fake token”\n```\n\nEditing this file didn’t trigger any HTTP requests, which was good, I thought. I then opened a completely unrelated `pyproject.toml,`\n\nand started typing in it.\n\nLo and behold, the following completion request went out while I was doing it:\n\n```\n{\n    \"prompt\":\"TEST_ENV_VAR_SECRET=\\\"mysecretenvvar\\\"\\n\\nT\",\n    \"suffix\":\"\",\n    \"max_tokens\":500,\n    \"temperature\":0,\n    \"top_p\":1,\n    \"n\":1,\n    \"stop\":[\"\\n\\n\\n\",\"\\n```\"],\n    \"stream\":true,\n    \"extra\":{\n        \"language\":\"dotenv\",\n        \"next_indent\":0,\n        \"trim_by_indentation\":true,\n        \"prompt_tokens\":175,\n        \"suffix_tokens\":0,\n        \"context\":[\n           \"Path: .env\",\n           \"These are recently edited files. Do not suggest code that has been deleted.\\nFile: pyproject.toml\\n--- a/file:///Users/rafaelpierre/copilot-mitm/pyproject.toml\\n+++ b/file:///Users/rafaelpierre/copilot-mitm/pyproject.toml\\n@@ -18,4 +18,4 @@\\n     \\\"polars>=1.41.0\\\",\\n ]\\n \\n+# testing\\n- --- IGNORE ---\\nFile: config.ini\\n--- a/file:///Users/rafaelpierre/copilot-mitm/config.ini\\n+++ b/file:///Users/rafaelpierre/copilot-mitm/config.ini\\n@@ -1,2 +1,3 @@\\n TEST_CONFIG=\\\"test-config\\\"\\n \\n+# test .env\\nEnd of recent edits\"\n        ]\n    },\n    \"code_annotations\":false\n}\n```\n\nMy first thought was: fine, I’ll just disable Copilot for\n\n`.env`\n\nfiles. Turns out it was already disabled; I had forgotten about it.It wouldn’t have mattered; the request was fired from keystrokes in the\n\npyproject.tomlfile, where inline completions were happily enabled.\n\n**Mental note**: turning inline completions off for `.env`\n\nitself or any other “*secret*” extension changes nothing, because the request is not being triggered by it. But other requests can be triggered.\n\n### Asking Copilot to refresh my memory\n\nI had seen a `session_store_sql`\n\ntool definition in the system prompts for many of the completion requests that I intercepted. Here is the tool description obtained from one such request:\n\n```\nQuery the local session store containing history from past coding sessions.\n\nUses SQLite syntax (NOT DuckDB or Postgres).\n\nSQL queries are read-only ‚Äî only SELECT and WITH are allowed.\n\nUse `datetime('now', '-1 day')` for date math (NOT `now() - INTERVAL '1 day'`), FTS5 `MATCH` for text search.\n\nTables: `sessions`, `turns`, `session_files`, `session_refs`, `checkpoints`, `search_index`.\n\nFor column details and query patterns, use the **chronicle** skill.\n\nActions: 'query' (execute SQL ‚Äî supports JOINs, FTS5 MATCH, aggregations), 'reindex' (rebuild index from debug logs).\n```\n\nHowever, I didn’t see any tool call results being sent back after that. The tool was probably not being called. To double check, I went on and tried to force a tool call by asking a simple question in the chat: *“What did I work on this week?”*.\n\nWhat followed was a back-and-forth between the model and a local SQLite database called `session-store.db`\n\n, which I didn’t know existed:\n\nAs I learned by looking into these conversations, `session_store_sql`\n\nis part of [Copilot’s Chronicle tool](https://github.blog/changelog/2026-06-02-gain-insights-across-your-agent-sessions-with-chronicle/), which lets it run SQL queries against `session-store.db`\n\n. This database stores session summaries, repos and branches you have worked on.\n\nIt also stores all of your prompts, along with their corresponding LLM responses. Copilot is keeping a queryable history of everything you’ve asked it, and reaching into that history when it’s needed.\n\nOne thing that stood out was that the model didn’t know the schema ahead of time. It initially tried the query below, **which failed**.\n\n```\n# Tool definition gets sent\n\n{\n    \"type\":\"function_call\",\n    \"name\":\"session_store_sql\",\n    \"arguments\":\"{\n        \\\"action\\\":\\\"query\\\",\n        \\\"description\\\":\\\"Fetch recent session activity for the past week\\\",\n        \\\"query\\\":\\\"SELECT s.id, s.start_time, s.title, t.turn_index, t.role, t.content FROM sessions s JOIN turns t ON t.session_id = s.id WHERE s.start_time >= datetime('now', '-7 days') ORDER BY s.start_time, t.turn_index;\\\"\n    }\",\n    \"call_id\":\"call_Ay35CDeV0EFXtvFI8l3VgbWI\"\n}\n\n# Tool gets executed locally, results are sent back to the agent/LLM:\n\n{\n    \"type\":\"function_call_output\",\n    \"call_id\":\"call_Ay35CDeV0EFXtvFI8l3VgbWI\",\n    \"output\":\"Error: no such column: s.start_time\"\n}\n```\n\nIt then introspected the schema metadata to find table definitions. After that, it was finally able to get some records from my local SQLite database.\n\n```\n# Session Store SQLite DB introspection tool call\n\n{\n    \"type\":\"function_call\",\n    \"name\":\"session_store_sql\",\n    \"arguments\":\"{\n        \\\"action\\\":\\\"query\\\",\n        \\\"description\\\":\\\"Inspect session store schema\\\",\n        \\\"query\\\":\\\"\n            SELECT name, sql\n            FROM sqlite_schema\n            WHERE type IN ('table','view');\n        \\\"\n    }\",\n    \"call_id\":\"call_wY9dGEI4DSYbOXzpzPg3JTGN\"\n}\n\n# Introspection tool call results get sent back to agent/LLM:\n\n{\n    \"type\":\"function_call_output\",\n    \"call_id\":\"call_wY9dGEI4DSYbOXzpzPg3JTGN\",\n    \"output\":\"Results: 13 rows (source: local)\n        | name | sql |\n        | --- | --- |\n        | schema_version | CREATE TABLE schema_version (\\n\\t\\t\\t\\tversion INTEGER NOT NULL (...)\\\n    \",\n}\n```\n\nEventually I became curious about querying my session data and finding out what else was stored there. So I started by looking at the metadata.\n\n``` bash\n$ sqlite3 ~/Library/Application Support/Code/User/globalStorage/github.copilot-chat/session-store.db\n\n# Output\n\nCREATE TABLE turns (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  session_id TEXT NOT NULL REFERENCES sessions(id),\n  turn_index INTEGER NOT NULL,\n  user_message TEXT,\n  assistant_response TEXT,\n  timestamp TEXT DEFAULT (...),\n  UNIQUE(session_id, turn_index)\n);\n```\n\nAs you can see, `user_message`\n\nand `assistant_response`\n\nare stored in plain text. Let’s query some of these manually.\n\n``` bash\n$ sqlite3 session-store.db \"SELECT substr(user_message,1,60) FROM turns LIMIT 5;\"\nWhat is ML?\nhello\ntesting\n```\n\nThese were some messages I had sent to Copilot previously to test my mitmproxy capture, so once again, no surprises. But what about messages that could potentially include something a bit more… *problematic*?\n\nTo find that out, I sent Copilot a chat message containing fake secret data: a fake GitHub token, a fake AWS key, a connection string with a password in it.\n\nThen, I went back to the database to see what had been written.\n\n``` bash\n$ sqlite3 session-store.db \"SELECT user_message FROM turns \\\n    WHERE user_message LIKE '%ghp_%' OR user_message LIKE '%postgres://%';\"\n\n...\nGITHUB_TOKEN=ghp_«fake token, stored exactly as typed»\nDATABASE_URL=postgres://admin:«password»@db.example.com:5432/prod\n...\n```\n\nI’ll admit, I got tempted to establish *“All there, in plain text”* as the headline.\n\nBut although this is true, my conclusion was actually less alarming - and actually more interesting, I would argue: **AI coding tools are becoming stateful systems.**\n\nAI coding tools are becoming stateful systems. They increasingly combine\n\nuser workspace + recent edits + conversations + tools + history + model routing.\n\nEach new source of context improves usefulness and increases the amount of developer state that the system can access. But it also brings additional challenges: increasing context bloat, data confidentiality and privacy concerns.\n\nWhile I enjoyed doing the reverse engineering exercise, I also became curious to see if my assumptions were grounded actual code. To confirm those, I needed to go to the code.\n\n## Reconciling these findings with the source code\n\n### Unencrypted Session Store\n\nSession store code is part of the Chronicle extension, and it lives in [sessionStore.ts](https://github.com/microsoft/vscode/blob/main/extensions/copilot/src/platform/chronicle/node/sessionStore.ts). The table definition is exactly what I’d seen on disk: `user_message`\n\nand `assistant_response`\n\nas plain text, no column-level masking or anything like that.\n\nBut the *schema* itself doesn’t tell you whether something scrubs the data on the way in. The write path does. Here’s the insert that records each turn:\n\n```\nINSERT INTO turns (session_id, turn_index, user_message, assistant_response, timestamp)\nVALUES (?, ?, ?, ?, ?)\n```\n\n…and the values bound to it:\n\n```\nturn.session_id,\nturn.turn_index,\nturn.user_message ?? null,\nturn.assistant_response ?? null,\nturn.timestamp ?? new Date().toISOString(),\n```\n\n`turn.user_message`\n\ngoes in as-is. I searched the code for any redaction, sanitization, secret-filtering, or masking in the write path. Nothing, there’s no scrubbing step. The plaintext storage isn’t a bug or a missed edge case; it’s simply what the code does.\n\nThat answers the first question: it’s deliberate, in the sense that nothing was ever built to prevent it.\n\n### To leak or not to leak\n\nThe *“recently edited files”* string I saw on the mitm capture comes from [recentEdits.tsx](https://github.com/microsoft/vscode/blob/main/extensions/copilot/src/extension/completions-core/vscode-node/lib/src/prompt/components/recentEdits.tsx). The default sliding window behavior is hardcoded: up to **20 files, 8 edit summaries, and 3 lines of context** around each change, which is how a line I hadn’t touched (the one with the fake secret in it) became part of an HTTP request to Copilot API.\n\nThere’s no default .env rule anywhere. On an individual plan, nothing treats .env as special, nor is there any integration with the current space’s\n\n`.gitignore`\n\n.\n\nThere’s an exclusion gate, but it’s tied to a “*repository policy*”, a *Business/Enterprise* GitHub feature and admin-controlled.\n\n## Parting words\n\n### With great power come great responsibilities\n\nThis turned out to be a great exercise in understanding how an AI coding tool implements its harnesses. I believe a lot of these details and practices can be absorbed by different teams building their own AI systems.\n\nSome of the questions I often ask myself while building such systems remain after all this. What context should get injected? What should be sent to the model? What should stay local? Which tools should the model be able to call? What gets stored in short term memory? What gets promoted to long term memory?\n\n### Context is becoming the product\n\nIncreasingly, I think **context is becoming the product.**\n\nModels and SOTA benchmark results matter, of course. But the real differentiation between AI coding tools - *and AI tools in general, for that matter* - seems to be shifting toward how well they assemble the right context: your code, recent changes, actions, conversations, tools, history, and whatever else might help solving the task at hand. This creates two challenges.\n\nThe first is an engineering problem: more context doesn’t necessarily translate to better context. The challenge is keeping it lean, relevant and [cache friendly](https://www.lighthousenewsletter.com/p/cutting-anthropic-token-costs-and), without drowning the model in prompt bloat.\n\nThe second is around privacy and confidentiality. The more a harness collects and persists contextual data, the more carefully it needs to define what can cross boundaries - between files, sessions, machines and ultimately, the model API.\n\nCopilot is clearly moving in this direction, and some of what I’ve found is clever. Some of it brought me an awkard feeling. And for now, none of it convinced me to become a paying customer again.\n\nBut it did convince me of something else. If you’re building AI applications, reverse engineering and studying the harness around models might teach you more than studying the model itself.\n\n*I hope you enjoyed this article. If you have any questions, if this resonates, if you have suggestions, reply to this email or drop a comment - I read all of them.*", "url": "https://wpnews.pro/news/what-i-learned-by-putting-github-copilot-behind-a-mitm-proxy", "canonical_source": "https://www.lighthousenewsletter.com/p/i-put-github-copilot-behind-a-mitm", "published_at": "2026-08-11 10:40:47+00:00", "updated_at": "2026-08-11 13:42:34.237045+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools"], "entities": ["GitHub Copilot", "VS Code", "Electron", "Slack", "Cursor", "Notion", "ChatGPT Desktop", "Claude Desktop"], "alternates": {"html": "https://wpnews.pro/news/what-i-learned-by-putting-github-copilot-behind-a-mitm-proxy", "markdown": "https://wpnews.pro/news/what-i-learned-by-putting-github-copilot-behind-a-mitm-proxy.md", "text": "https://wpnews.pro/news/what-i-learned-by-putting-github-copilot-behind-a-mitm-proxy.txt", "jsonld": "https://wpnews.pro/news/what-i-learned-by-putting-github-copilot-behind-a-mitm-proxy.jsonld"}}