Welcome to Chapter 3!
In the previous chapter, OS Protocol Registration, we taught the Operating System how to recognize our links. When a user clicks claude-cli://..., the OS now launches our application and passes that link as a string.
But here is the problem: We cannot trust that string.
In this chapter, we will build the security layer. We will take that raw, potentially dangerous text and convert it into a safe, clean "Action Object" that our application can understand.
Think of your application as an exclusive Club.
If you let everyone in without checking them, someone might bring in something dangerous. In the world of programming, a malicious user might craft a link that looks like this:
claude-cli://open?q=Hello & rm -rf /
If we just blindly passed this to a terminal, the computer might interpret & as "and then," and rm -rf / as "delete everything." This is called Command Injection.
We need a Bouncer at the door. The bouncer's job is to:
To build our Bouncer, we need to understand three concepts.
Our links follow a strict format. If the link doesn't look like this, we reject it immediately.
Protocol Action Parameters
โ โ โ
claude-cli:// open ?q=hello&repo=my-app
Computers have invisible characters that control text.
If a URL contains a "New Line" character, the terminal might think the user pressed "Enter" to execute a command. We must reject any string containing these characters.
We don't want to pass strings around our app. We want an object.
"claude-cli://open?q=hi"{ query: "hi", repo: undefined }
Let's look at parseDeepLink.ts. This is where our Bouncer lives.
The function parseDeepLink is the main door. The first thing it does is ensure the ID card is valid.
// parseDeepLink.ts
export function parseDeepLink(uri: string): DeepLinkAction {
// 1. Normalize and Check Protocol
const normalized = uri.startsWith('claude-cli://') ? uri : null
if (!normalized) {
throw new Error(`Invalid deep link: expected claude-cli://`)
}
// ... continue to Step 2
Explanation: If the link doesn't start with our specific protocol, we throw an error immediately. We don't even look at the rest of it.
We don't want to write our own complex text parser. Browsers and Node.js have a built-in URL class that is very good at breaking strings apart.
// 2. Use the built-in URL parser
let url: URL
try {
url = new URL(normalized)
} catch {
throw new Error(`Invalid deep link structure`)
}
// 3. Extract the raw parameters
const rawQuery = url.searchParams.get('q')
const repo = url.searchParams.get('repo')
Explanation: The URL class handles the messy work of finding where ? starts and where & separates items. It gives us the values directly.
Now we have the values. But are they safe? We need a helper function to check for those dangerous invisible characters we talked about.
// Helper: Returns true if string has dangerous ASCII codes
function containsControlChars(s: string): boolean {
for (let i = 0; i < s.length; i++) {
const code = s.charCodeAt(i)
// 0x1f and below are control chars (like New Line)
if (code <= 0x1f || code === 0x7f) {
return true
}
}
return false
}
Explanation: We loop through every single letter. If we find a character code that represents a system command (like "Backspace" or "Escape"), we flag it.
Now we apply the rules to our specific parameters.
// Inside parseDeepLink...
// Rule: Repo must look like "owner/name"
if (repo && !/^[\w.-]+\/[\w.-]+$/.test(repo)) {
throw new Error(`Invalid repo format`)
}
// Rule: Query must not be too long or contain control chars
if (rawQuery) {
if (containsControlChars(rawQuery)) {
throw new Error('Security: Query contains control characters')
}
// Prevent massive buffers crashing the terminal
if (rawQuery.length > 5000) {
throw new Error('Security: Query too long')
}
}
Explanation:
../ to navigate up your file system.Finally, if everything passes, we create the structured object.
// Return the safe, structured object
return {
query: rawQuery,
repo: repo
}
}
This object is now "clean." It has passed the security checkpoint.
Here is how the data transforms from a dangerous string to a safe object.
While the code above is simplified, our production parseDeepLink.ts handles a few extra edge cases:
partiallySanitizeUnicode() to strip these out./ or C:\) to prevent ambiguity.In this chapter, we built the Parser and Sanitizer.
We learned:
Now that we have a clean object, we know what the user wants to do. But before we actually execute it, we need to show the user exactly what is about to happen. We need to be transparent.
In the next chapter, we will build the UI that displays this "Action Object" to the user for final confirmation.
Next Chapter: Session Provenance & Security UI
Generated by Code IQ