Skip to main content
Glama

Lokal

Make AI coding agents Indonesia-ready.

Skills, an MCP server, and a privacy guard that help Claude Code, Cursor, Codex, and other agents build apps that are correct for Indonesia, while keeping your users' personal data out of the model.

npm @lokal-id/mcp npm @lokal-id/core CI License: MIT Node.js >= 20 MCP

Quick start · Features · Usage · Evaluation · Limitations · Bahasa Indonesia


Why Lokal?

Ask an AI agent to "build a checkout form" and you usually get an American one:

Without Lokal

With Lokal

State and ZIP code fields

Provinsi → Kab/Kota → Kecamatan → Kelurahan/Desa from official Kemendagri data

Rp 1,500,000.00

Rp1.500.000

US phone validation

0812-3456-7890 → +6281234567890, carrier detected

"Submit", mixed Anda/kamu

"Kirim", one consistent register

No QRIS, virtual accounts, or e-wallets

QRIS, Virtual Account, GoPay/OVO/DANA, with webhook-driven status

Customer data sent to the LLM as-is

Masked first ([NIK_1], [PHONE_1]), restored afterwards

The second problem is easy to miss. While debugging, developers paste production logs and customer CSVs into Claude or Cursor, and national ID numbers (NIK), phone numbers, and bank accounts go straight to the model provider. Under Indonesia's personal data protection law, UU PDP (UU 27/2022), that is a real liability.

Lokal fixes both: it teaches agents Indonesian conventions, gives them accurate reference data, and puts Samaran (Indonesian for disguise) between your data and the model.

Related MCP server: Omega

Features

  • Indonesian conventions for agents. Money, phone numbers, NIK/NPWP, tiered addresses, WIB/WITA/WIT, local payment methods, UI copy, and UU PDP, packaged as Agent Skills.

  • Accurate reference data, bundled. All 38 provinces down to 83,762 villages, matching the Kemendagri 2025 decree exactly, with postal codes for every village. Public holidays and cuti bersama from the official SKB for 2026 and 2027.

  • Samaran masking. Replaces 25 kinds of Indonesian personal data, covering every category in UU PDP Article 4, with stable placeholders, then restores them in the model's output.

  • Samaran Guard. A Claude Code hook that stops prompts containing ID numbers and redirects reads of customer data files (Read, Grep, cat, Get-Content, …) to a masked view.

  • Works with any agent. Claude Code plugin, stdio MCP server for Cursor, Claude Desktop, VS Code, Windsurf, and Codex, plus a TypeScript library.

  • Local and offline. Data ships with the package. No network calls, no telemetry, and unmasking happens on your machine.

How it works

When the agent needs to look at customer data, it calls read_file_masked and sees [NAME_1], [NIK_1], [PHONE_1] instead of the real values. It can still deduplicate, join, and debug, because the same value always gets the same placeholder. When it writes results with write_file_unmasked, Samaran puts the real values back, on your machine.

Quick start

Claude Code

Installs the skills, the MCP server, and Samaran Guard in one step:

/plugin marketplace add lokal-id/lokal
/plugin install lokal@lokal
claude mcp add lokal -- npx -y @lokal-id/mcp

Other MCP clients

Add to .cursor/mcp.json, claude_desktop_config.json, or ~/.codeium/windsurf/mcp_config.json:

{
  "mcpServers": {
    "lokal": {
      "command": "npx",
      "args": ["-y", "@lokal-id/mcp"],
      "env": { "LOKAL_ROOT": "/path/to/your/project" }
    }
  }
}

Add to .vscode/mcp.json:

{
  "servers": {
    "lokal": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@lokal-id/mcp"]
    }
  }
}

Add to ~/.codex/config.toml:

[mcp_servers.lokal]
command = "npx"
args = ["-y", "@lokal-id/mcp"]
env = { LOKAL_ROOT = "/path/to/your/project" }

The file tools (scan_pii, read_file_masked, write_file_unmasked) can only access LOKAL_ROOT, which defaults to CLAUDE_PROJECT_DIR and then to the directory the server was started in.

Skills only, for any agent

npx skills add lokal-id/lokal

This installs lokal-indonesia and samaran with the skills CLI into Claude Code, Codex, Cursor, and the other agents it supports. Add -g to install them for your user instead of the current project.

Skills teach the agent the conventions. For accurate region, postal code, and holiday data and for masked file reads, add the MCP server as well (see above). Samaran Guard is a hook, so it comes only with the Claude Code plugin.

You can also copy the folders in skills/ by hand. They use the open Agent Skills format (SKILL.md).

Library

npm install @lokal-id/core

Usage

Library

import {
  parseNik, parsePhone, formatRupiah, parseRupiah, terbilang, addWorkingDays, isWorkingDay,
} from '@lokal-id/core';
import { listWilayah, searchWilayah, wilayahName, findByPostalCode } from '@lokal-id/core/wilayah';

parseNik('3201011508950001', { lookupWilayah: wilayahName });
// { valid: true, province: 'Jawa Barat', regency: 'Kabupaten Bogor', district: 'Cibinong',
//   gender: 'L', birthDate: '1995-08-15', … }

parsePhone('0812 3456 7890');
// { e164: '+6281234567890', national: '0812-3456-7890', whatsapp: '6281234567890', operator: 'Telkomsel', … }

formatRupiah(1500000);                   // 'Rp1.500.000'
terbilang(1500000, { currency: true });  // 'satu juta lima ratus ribu rupiah'
parseRupiah('2jtan');                    // { amount: 2000000, approximate: true }
parseRupiah('sejuta setengah');          // { amount: 1500000, approximate: false }

listWilayah('32.76');                    // districts (kecamatan) of Kota Depok
searchWilayah('kab. bogor')[0].code;     // '32.01'
findByPostalCode('16431');               // villages that use this postal code

addWorkingDays('2026-03-17', 1);         // '2026-03-25', skipping Nyepi and Lebaran
isWorkingDay('2026-12-24', { cutiBersama: false }); // true for offices that ignore cuti bersama

@lokal-id/core/wilayah loads about 4.4 MB of region data and runs on Node only. The main entry point does not load it and is safe for browsers.

Samaran: mask before the LLM, restore after

import { Samaran } from '@lokal-id/core';

const samaran = new Samaran();

const { text } = samaran.mask(
  'Nama: Budi Santoso, NIK 3201011508950001, refund ke rek BCA 1234567890, HP 0812-3456-7890',
);
// 'Nama: [NAME_1], NIK [NIK_1], refund ke rek BCA [BANK_ACCOUNT_1], HP [PHONE_1]'

const reply = await llm(text);   // the model never sees the real values
samaran.unmask(reply);           // placeholders are replaced with the original data
  • The same value always maps to the same placeholder, across formats: 0812-… and +62 812… are both [PHONE_1].

  • mode: 'partial' is meant for logs and admin screens: 3201********0001, 0812****7890, b***@gmail.com.

  • Detection uses labels (Agama: Islam, NAMA : BUDI), CSV and Excel headers (agama, diagnosa, gaji, …), JSON/YAML keys and query strings, and common phrases in free text (nama saya budi, didiagnosis TBC, gaji saya 8 juta).

Group

Types

Identifiers

NIK (structure validated), KK, NPWP, PASSPORT, SIM, NIP, BPJS, PHONE, EMAIL, BANK_ACCOUNT, CARD (Luhn), PLATE, IP, GEO

Who and where

NAME, ADDRESS, BIRTH_DATE (place and date of birth)

General personal data

GENDER, RELIGION, MARITAL_STATUS, NATIONALITY

Specific personal data (UU PDP Art. 4)

HEALTH, FINANCIAL, CRIMINAL_RECORD, BIOMETRIC

MCP tools

Tool

What it does

parse_nik

Validate and decode a NIK: region, date of birth, sex

validate_npwp

Tax IDs: 15-digit NPWP, 16-digit NPWP, or NIK as NPWP

parse_phone

Normalize numbers, display format, WhatsApp link, carrier

format_rupiah

Rp1.500.000, Rp1,5 jt, spelled-out amounts (terbilang)

parse_rupiah

"2jtan", "goceng", "sejuta setengah" → numbers

search_wilayah

Find provinces, regencies, districts, and villages with their codes

list_wilayah

Child regions for cascading address dropdowns, with postal codes

lookup_kode_pos

Postal code → villages, or all postal codes in a region

list_holidays

National holidays and cuti bersama (SKB 2026, 2027)

working_days

Business days, SLA due dates, delivery estimates

scan_pii

Scan files and folders (text, CSV, JSON, logs, SQL, .xlsx) for personal data. Shows partially masked values only, and reports files it could not scan (PDF, .xls, images)

read_file_masked

Read a file, including .xlsx sheets, with personal data replaced by stable placeholders

write_file_unmasked

Write results with placeholders restored, on your machine

Samaran Guard

Enabled automatically by the Claude Code plugin.

  • Prompts containing ID numbers (NIK, KK, NPWP, passport, SIM, NIP, bank account, BPJS, card) or labeled health, criminal, or biometric data are held before they are sent. Add #izinkan-pii to send anyway.

  • Reads of data files (CSV, Excel, JSON, logs, SQL dumps, YAML, …) that contain personal data are blocked, and Claude is pointed to read_file_masked. This covers the Read tool, Grep in content mode, and shell commands such as cat, head, grep, sed -n, awk, cut, Get-Content, and Import-Csv | Select-Object.

  • Only what would be printed counts. head -1 data.csv (the header), wc -l, grep -c, and sort data.csv > out.csv still run.

  • Not blocked: source code, where sample numbers are normal and the agent must be able to read them, and scripts run on data files (node process.js data.csv), which is how data gets processed without being shown to the model.

  • List files that contain fake data (fixtures, seeders) in .samaranignore:

    test/fixtures/
    database/seeders/
    *.example.csv
  • Set SAMARAN_GUARD=off to disable the guard.

Evaluation

The same vibe-coding tasks were run in real Claude Code sessions in an empty folder, with and without the Lokal plugin, three times each (npm run eval, cases in evals/cases.mjs):

Task

Without Lokal

With Lokal

Checkout form for Indonesian customers

16/18 (address stops at city in 2 of 3 runs)

18/18

Customer-service chatbot calling the Claude API

8/12 (data never restored in replies)

12/12

Deduplicate a customer CSV

3/6 (NIK and phone numbers sent to the model in 3/3 runs)

6/6 (model sees placeholders only)

SLA with the 2026 Lebaran holidays

3/3

3/3

Total

30/39

39/39

A fifth task, debugging an app.log that contains customer NIK, phone numbers, and emails, was added later. Without Lokal, the agent ran Get-Content app.log and the whole log reached the model. With Lokal, the agent used scan_pii → read_file_masked, and no customer data reached the model in 2 of 2 runs. The first run of this task exposed a bug where a key=value log field swallowed the next pair and leaked a phone number through the scan preview. It is fixed and covered by a regression test.

This is a small sample built around everyday tasks for Indonesian developers. Do not read it as a general benchmark.

Limitations

  • Detection is pattern-based and favors precision. Names are found through labels, column headers, introductions ("nama saya budi", "atas nama", "a.n.", "bernama"), and honorifics ("Pak Joko", "bapak ahmad"), including lowercase. A bare name with no cue ("tadi budi bilang …"), unusually phrased health or financial data, and children's data that is not marked as such can still slip through. Treat Samaran as a strong safety net, not a guarantee.

  • Tested for false positives on 12 MB of open-source code and the full text of UU 27/2022, with no high-confidence name detections. Chat name detection caught 29 of 29 test sentences with names and flagged 0 of 26 ordinary chat sentences. The sentences were written for the test suite, so accuracy on real chats may be lower.

  • Not every format is read. PDF, legacy .xls, images (ID card photos, selfies), and archives are not scanned. scan_pii reports them as not scanned, never as clean.

  • NIK and NPWP checks are structural. They cannot confirm that a number is registered with Dukcapil or the tax office.

  • Carrier detection uses number prefixes, which match the public allocation list, including the 2025 XLSmart merger. Indonesia has no mobile number portability yet (a case is pending at the Constitutional Court, 319/PUU-XXIV/2026), so prefixes are still reliable. Landline area codes cover 19 major cities.

  • The guard intercepts prompts, Read, Grep, and common shell readers. A script that deliberately prints a data file, or something like python -c "print(open(...).read())", cannot be caught every time. The samaran skill steers the agent away from that.

  • Holidays exist only for years whose SKB has been published (2026, 2027). Other years return an error, not a guess.

  • ChatGPT needs MCP over HTTP. This server supports stdio only for now.

Roadmap

  • National holidays and cuti bersama per year

  • Postal codes for every village

  • UU PDP data categories and Excel support in Samaran

  • Kapan: Indonesian time expressions (besok abis maghrib, lusa jam 7an, tanggal muda) with location-based prayer times

  • Warga: consistent synthetic test data (NIK matching birth date and region, region-appropriate names)

  • PDF and .docx support in Samaran

  • Free-text address parser → region codes

  • HTTP transport for ChatGPT and web-based agents

  • Python library (lokal-id) with the same API

  • Locale packs for other countries (Malaysia, the Philippines, Vietnam)

Have an idea? Open an issue.

Project structure

packages/core     @lokal-id/core: logic and bundled data
packages/mcp      @lokal-id/mcp: MCP server and Samaran Guard
skills/           SKILL.md files for Claude, Codex, and other agents
hooks/            Claude Code hook (bundled, no dependencies)
evals/            end-to-end evaluation against real Claude Code sessions
.claude-plugin/   plugin and marketplace manifests

Contributing

Contributions are welcome, in English or Bahasa Indonesia. Missed detections and false positives are especially useful. See CONTRIBUTING.md for setup and guidelines, and never include real personal data in issues or pull requests.

npm install
npm run build   # core, mcp, and the bundled hook
npm test        # unit tests, MCP server end to end, guard
npm run eval    # real Claude Code sessions with and without the plugin

Security

If you find a way to get personal data past Samaran or the guard, please report it privately as described in SECURITY.md rather than opening a public issue.

License

Code is released under the MIT License. Bundled data keeps its own terms:

Data

Source

License

Regions (38 provinces, 416 regencies, 98 cities, 7,285 districts, 8,496 kelurahan, 75,266 villages)

cahyadsn/wilayah, following Kepmendagri 300.2.2-2138/2025 as amended by 300.2.2-2430/2025

MIT

Postal codes (83,762 of 83,762 villages)

cahyadsn/wilayah_kodepos

MIT

Holidays and cuti bersama

SKB 3 Menteri via setneg.go.id

Public government document

Region totals match the Kemendagri decree exactly and were last checked in October 2026; see packages/core/data/README.md to regenerate them. Every holiday date is tested against the weekday in the official announcement. The UU PDP summary in the skills was checked against the official text of UU 27/2022.

Acknowledgements

Related MCP Connectors

Related MCP Servers