# How ai-job-search Handles External API Integrations: A Modular Security-First Approach

> Discover how ai-job-search secures external API integrations using modular skill handlers, OAuth, and domain allow-listing for robust protection and efficient data access.

- Repository: [Mads Lorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search)
- Tags: architecture
- Published: 2026-07-25

---

**The ai-job-search framework isolates every external API interaction within dedicated skill modules and command handlers, using OAuth authentication for Gmail and Notion, public endpoints for job boards, and strict domain allow-listing to prevent unauthorized outbound traffic.**

The MadsLorentzen/ai-job-search repository manages external API integrations through a decentralized skill architecture that keeps the core automation logic agnostic of third-party implementation details. Rather than embedding HTTP clients throughout the codebase, each service—whether a job board or productivity suite—lives in its own self-contained CLI tool under specific directories. This design ensures that authentication flows, rate limiting, and data parsing remain encapsulated and maintainable while keeping the primary keyword of external API integrations secure and modular.

## Skill-Based Architecture for Job Portals

Each job search integration is implemented as a standalone CLI "skill" located in the `.agents/skills/` directory, executed via Bun to fetch and normalize posting data.

### LinkedIn Unauthenticated Search

The **LinkedIn search** skill targets the public, unauthenticated `jobs-guest` Voyager API endpoint. Located in [`.agents/skills/linkedin-search/cli/main.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/main.ts), the TypeScript client constructs a request URL and streams JSON results to stdout.

```typescript
// .agents/skills/linkedin-search/cli/main.ts (executed by bun)
const response = await fetch(
    `https://www.linkedin.com/voyager/api/jobs-guest/jobs?keywords=${encodeURIComponent(query)}&location=${encodeURIComponent(location)}`
);
const data = await response.json();
console.log(JSON.stringify(data.results));

```

### freehire.me Public REST API

Similarly, the **freehire.me** integration in [`.agents/skills/freehire-search/cli/main.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/freehire-search/cli/main.ts) queries the freehire.me public REST API, retrieving regional and remote job listings via standard HTTP GET requests without requiring API keys.

```typescript
// .agents/skills/freehire-search/cli/main.ts
const response = await fetch(
    `https://freehire.me/api/v1/jobs?region=${REGION}&remote=${REMOTE}`
);

```

## Productivity Suite Synchronization

Beyond job aggregation, the project integrates with personal productivity tools through Claude command definitions that delegate to authenticated connectors.

### Gmail Sync via OAuth 2.0

The `/gmail-sync` command, defined in [`.claude/commands/gmail-sync.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/gmail-sync.md), instantiates a `GmailConnector` using a stored OAuth token to search for status-update messages—such as interview invitations or rejection notices—then proposes state changes to the tracking system only after user approval.

```python

# Conceptual implementation from .claude/commands/gmail-sync.md

gmail = GmailConnector(oauth_token=USER_TOKEN)
messages = gmail.search('subject:"Interview" label:unread')
for msg in messages:
    propose_outcome_update(msg)

```

### Notion MCP Server Integration

The `/notion-sync` command pushes a read-only view of the local tracking CSV to a Notion database using the official Notion MCP server. According to the command documentation in [`.claude/commands/notion-sync.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/notion-sync.md), this one-way sync relies on OAuth authentication managed by the connector rather than manual secret keys.

## Security Controls and Local Data Governance

The project implements strict safeguards to prevent unauthorized external calls and to handle sensitive data locally where possible.

### Domain Allow-Listing Enforcement

All outbound network requests are gated by **security_guards.py**, which maintains an explicit allow-list of permitted domains. This module, located at [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py), ensures that the system cannot silently invoke unknown or untrusted APIs, acting as a network-level sandbox for the skill modules. The guards are protected by CI checks to prevent accidental modification of permitted domains.

### Local Salary Benchmarking

Unlike the job search skills, the [`salary_lookup.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/salary_lookup.py) utility operates entirely on local datasets (CSV or Excel files) without invoking external APIs. An optional helper script, [`tools/convert_salary_excel.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/convert_salary_excel.py), transforms Excel files into JSON for local consumption, ensuring compensation data never leaves the user's environment.

## Summary

- The ai-job-search project isolates external API integrations within modular CLI skills under `.agents/skills/` and command definitions in `.claude/commands/`.
- **LinkedIn** and **freehire.me** integrations use unauthenticated public REST endpoints executed via Bun TypeScript CLIs.
- **Gmail** and **Notion** synchronization rely on OAuth 2.0 connectors to access user data securely without hardcoded secrets.
- **Security guards** in [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py) enforce a strict domain allow-list, blocking unauthorized outbound traffic.
- Salary data processing remains entirely local via [`salary_lookup.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/salary_lookup.py), eliminating external API dependencies for sensitive financial information.

## Frequently Asked Questions

### Does ai-job-search require API keys for job searching?

No. The LinkedIn and freehire.me skills query public, unauthenticated endpoints that do not require API keys. Only the Gmail and Notion integrations require OAuth authentication to access private user data, managed through secure connectors rather than static credentials.

### How does the project prevent unauthorized API calls?

The [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py) module implements an explicit domain allow-list that validates every outbound request against permitted hosts. This prevents skill modules from contacting unknown or malicious endpoints, and the file is protected by CI guards to ensure the allow-list cannot be bypassed.

### Is the Notion integration bidirectional?

No. According to the command documentation in [`.claude/commands/notion-sync.md`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/commands/notion-sync.md), the Notion sync is one-way and read-only, pushing a view of the local tracking data to Notion without writing changes back to the source CSV.

### What runtime executes the job search skill scripts?

The skill modules are written in TypeScript and executed using **Bun**, as evidenced by the fetch-based HTTP calls in [`.agents/skills/linkedin-search/cli/main.ts`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.agents/skills/linkedin-search/cli/main.ts) and the CLI entry points throughout the `.agents/skills/` directory.