Threat Intelligence

Osquery Extensions: Adding Custom Tables and Capabilities

Osquery ships with hundreds of tables, and you will still want one it does not have. Extensions add custom tables, config, and logging as a separate process. Here is when and how to build one.

Alex Gibson, Co-Founder and Principal at Artemes AI
Alex Gibson
Co-Founder, Principal
Jul 22, 2026 9 min read
Abstract visualization of a custom extension process connected to the osquery daemon over a socket

Osquery ships with hundreds of tables, and the moment you deploy it at scale you will want one it does not have. That gap is not a flaw. It is what extensions are for.

Extensions let you add custom tables, config sources, and log destinations without forking the project or waiting for a table to land upstream. An extension is a small program that runs beside the osquery daemon and answers queries for the tables it registers. To an analyst writing SQL, a table from an extension looks exactly like a built in one. Under the hood it is a separate process, which turns out to be the most important design decision in the whole system.

This guide covers what extensions are, when to reach for one instead of a scheduled query, how they connect to the daemon, and the permission and packaging details that decide whether your extension loads or gets rejected.

Infographic

How an extension talks to osquery

An extension is a separate process. It registers its tables with the daemon over a local socket, and the daemon queries it like any built in table.

The process model connecting the osquery daemon to a custom extensionTwo boxes side by side. On the left, the osquery daemon process, which holds the SQL engine and the built in tables. On the right, a separate extension process, which holds custom tables, and optional config and logger plugins. Between them, a line labeled Thrift over a local UNIX domain socket, with arrows in both directions. An arrow from the daemon to the extension is labeled query for rows. An arrow from the extension back is labeled returns rows. Below, a note reads: because the extension is a separate process, if it crashes the daemon keeps running. Osquery treats it as a monitored worker with memory and CPU limits.osqueryd processSQL engine (SQLite)280+ built in tablesscheduler and loggerruns the query planextension processyour custom tablesconfig plugin (optional)logger plugin (optional)your_extension.extquery for rowsreturns rowsThrift · UNIX socketSeparate process means a crashing extension does not take the daemon down.Osquery runs it as a monitored worker with the same memory and CPU limits.

What are osquery extensions?

Osquery extensions are external programs that add capability to osquery through an API built on Apache Thrift, a framework for cross process communication similar in spirit to gRPC. A single extension can register any number of plugins, and there are three kinds that matter: table plugins that expose new data as SQL, config plugins that feed osquery its configuration from a custom source, and logger plugins that send results to a destination the core does not support out of the box. The mechanics are laid out in the osquery extensions documentation.

The reason to care about the difference between a table and an extension is scope. A scheduled query reshapes data osquery already collects. An extension teaches osquery to collect something new. If the fact you need lives in an internal inventory API, a proprietary database, or an application log format nobody upstream has heard of, no combination of existing tables will surface it. An extension will. New to the table model? Our guides to the most useful osquery tables and writing osquery queries cover the built in surface you should exhaust first.

When should you write an extension?

Not as often as people think. The core schema already exceeds 280 tables and the project keeps shipping new ones on a monthly cadence, visible in the osquery release history. Before you write code, check the schema. The table you want may already exist, and a table you maintain yourself is a table you have to keep working across operating system changes forever.

Write an extension when the answer is not reachable any other way. Three honest cases. First, data that only your organization has: a CMDB, an internal licensing service, a homegrown agent's status file. Second, a system osquery cannot read natively, such as a cloud provider API you want joined against local host state. Third, a config or logging path your deployment requires that the core does not support, which is exactly why large deployers like Facebook ship an internal extension bundling their own tables and config plugins.

Weigh it like the build decision it is. An extension is software you now own: it needs testing, packaging, signing, and a maintainer when the person who wrote it leaves. If a table would save your team two hours a week and takes a week to build and a day a quarter to maintain, the math works. If you are writing it because querying an API from SQL sounds elegant, it usually does not.

How do extensions connect to osquery?

Every extension runs as its own process and talks to the daemon over Thrift on a local UNIX domain socket. When osquery plans a query that touches an extension table, it asks the extension process for the rows and gets them back over that socket. The daemon treats the extension as a monitored worker and applies the same memory and CPU watchdog limits it applies to itself, which is the same protection model we cover in osquery performance tuning.

That separate process is the quiet genius of the design. Because your code is not linked into the daemon, a bug in your extension that would crash a monolithic agent instead crashes only the extension. The daemon notices, keeps serving its built in tables, and can restart the worker. You get to write a table in a memory managed language without putting the whole agent at risk.

The most common way to build one is the Go SDK, osquery-go, though C++ and Python are supported too. A table plugin defines its columns and a generate function that returns the rows. Columns come in four SQL types: TEXT, INTEGER, BIGINT, and DOUBLE. The generate function does the real work: call your API, read your file, query your database, and hand back a list of rows. The API is documented in the osquery SDK reference.

What does a minimal table look like?

Smaller than most people expect. A table plugin has two parts: a declaration of its columns and a generate function that returns rows as a list of maps. Everything specific to your data lives inside that one function. In Go, the skeleton of a table that joins a hostname to its owning team from an internal service looks like this:

func Columns() []table.ColumnDefinition {
  return []table.ColumnDefinition{
    table.TextColumn("hostname"),
    table.TextColumn("owning_team"),
  }
}
 
func Generate(ctx context.Context, q table.QueryContext)
  ([]map[string]string, error) {
  // call your API, read a file, query a database
  return []map[string]string{
    {"hostname": "web-01", "owning_team": "platform"},
  }, nil
}

Your main function registers that table with an extension manager server and starts it. From then on, an analyst can write SELECT * FROM host_ownership; and join it against any built in table, and nothing in the query hints that the data came from an extension rather than the core. That is the payoff: you add a column of organizational truth and it behaves like it was always part of osquery.

Two disciplines keep a table like this from becoming a liability. Keep the generate function fast, because it runs inside the query path and a slow call there stalls every query that touches the table. And handle failure cleanly, returning an error rather than a partial result, so a flaky upstream API shows up as a clear failure instead of quietly wrong rows. A table that lies is worse than a table that is missing.

How do you load an extension?

Two ways. For a quick test, load one by hand in the interactive shell:

osqueryi --extension /path/to/your_extension.ext

For a real deployment, autoload it. Point the daemon at a file listing your extension paths, one per line, and it launches each as a monitored child on startup.

# osquery.flags
--extensions_autoload=/etc/osquery/extensions.load
--extensions_timeout=3
--extensions_interval=3
 
# /etc/osquery/extensions.load
/usr/lib/osquery/extensions/my_extension.ext

Two rules the loader enforces without apology. The file name must end in .ext, and the executable must be owned by root, or Administrator on Windows, and not writable by any unprivileged account. If the permissions are loose, osquery refuses to load it and logs Extension binary has unsafe permissions. That check exists because an extension runs with the agent's privileges. A file any user can overwrite is a file any user can turn into code running as root. The rule is protecting you.

On Windows the ownership check is stricter because of permission inheritance. Setting the owner is not enough; you also lock down the parent directory and disable inherited permissions with icacls. For throwaway local testing you can pass --allow_unsafe to skip the check, but treat that flag as a lab tool. It has no place in a deployed configuration.

Once loaded, confirm your table is live by asking osquery's own registry:

SELECT name FROM osquery_registry
  WHERE registry = 'table' AND internal = false;

How do you ship an extension across a fleet?

Building the binary is the easy part. Getting it onto thousands of hosts safely is the part that decides whether the project survives contact with production. Compile a static binary for each operating system you support so the extension has no runtime dependencies to chase, and name every one with the required .extsuffix. Ship it inside your normal package, the same one that carries the osquery config, and set the file permissions in the package build rather than fixing them by hand on each host. The loader will reject a binary an unprivileged account can write, and you do not want to discover that at scale.

On each host, list the extension path in your extensions.load file and let the daemon autoload it. Version the extension the way you version the rest of your configuration, because an extension is now part of your deployment surface and a bad build can silently stop a table from populating across the fleet. This is the same operational discipline that keeps a scheduled query set healthy, covered in our osquery fleet management guide. Roll it out to a pilot group first, confirm the table populates and the daemon stays healthy, then widen. Because the extension runs as a monitored worker, a crash on one host is contained to that host, which makes a staged rollout low risk as long as you are watching the pilot.

What extensions do not fix

An extension widens what osquery can see. It does nothing for what happens after. A custom table that pulls risk scores from your CMDB still produces rows that someone has to read, correlate, and act on. Adding a data source multiplies the volume flowing into your pipeline, and volume without interpretation is just a bigger bill for the same blind spots. This is the same limit that shapes how we think about machine learning on endpoint telemetry: more collection raises the ceiling on what you can know and also raises the cost of not analyzing it.

This is where the design of a modern platform earns its keep. Artemes AI treats deep endpoint context as the input, not the product. The value comes from an analysis layer that reads that context, including custom signals an organization adds, and decides which findings are real and what the fix is. An extension gives you a new column of truth. Something still has to reason over the whole row.

Frequently asked questions

Do I need to know C++ to write an osquery extension?

No. Go is the most common choice thanks to the osquery-go SDK, and Python is supported as well. C++ is available if you want it, but most custom tables are simpler to write and maintain in Go.

Will an extension slow down or destabilize osquery?

A slow extension slows only the queries that touch its tables, and because it runs as a separate process, a crash does not take the daemon with it. Osquery applies its worker watchdog to extensions, so a runaway extension is contained the same way a runaway query is.

Why does osquery refuse to load my extension?

Almost always a permissions problem. The .ext file must be owned by root or Administrator and not writable by unprivileged users, and on Windows the parent directory permissions matter too. The log line "Extension binary has unsafe permissions" points straight at the cause.

Can extensions add config and logging, not just tables?

Yes. Config plugins let osquery pull its configuration from a custom source, and logger plugins send results to a destination the core does not support. Both are common reasons large deployments ship an extension at all.

The takeaway

Reach for an extension only after you have confirmed the table you need does not already exist, then treat the result as owned software with a real maintenance cost. Build it in Go, keep the generate function narrow and fast, lock the binary's permissions down so the loader accepts it, and autoload it through a flags file. The payoff is real: osquery stops being limited to what the project ships and starts answering questions specific to your environment. For putting that capability to work across thousands of hosts, our definitive osquery handbook and fleet management guide cover the deployment side.

Artemes AI

See which of your findings actually matter

Artemes AI combines deep endpoint context with AI-driven analysis to show which findings are actually exploitable on your hosts—not just everything a scanner can list. We are onboarding early access teams now.

Alex Gibson, Co-Founder and Principal at Artemes AI

Alex Gibson

Co-Founder, Principal

Alex writes about configuration drift, operational security evidence, endpoint telemetry, AI-assisted triage, and the practical work of turning signals into better remediation decisions.

Osquery
Endpoint Telemetry
Security Automation
Found this useful? Share it.