For the whole of PostgreSQL’s history until now, an extension had to live in exactly one place. CREATE EXTENSION foo read foo.control from the compiled-in extension directory, the one pg_config --sharedir points at, and it looked nowhere else. If you wanted an extension available, its files had to be in that directory, which in practice meant installed as root into the system tree or baked into your image. extension_control_path, new in PostgreSQL 18, ends that assumption.

It is a string, default $system, holding a list of directories, colon-separated (semicolons on Windows), that PostgreSQL will search for extension control files. The $system token stands for that original compiled-in directory, so you keep every extension the distribution ships while adding locations of your own: extension_control_path = '/opt/pg/share/postgresql:$system' looks in your directory first and the system one second. Whichever directory yields the control file first wins, and the extension’s SQL scripts are read from alongside it.

The thing to know is that this is only half of a pair. extension_control_path finds the control file and the SQL; it does not find the extension’s shared library, the actual .so or .dll with the C code in it. That is still located through dynamic_library_path, whose token is $libdir rather than $system but which otherwise works identically, because the two share the same path-searching machinery underneath. Point one at a nonstandard location and not the other, and CREATE EXTENSION will locate the control file and then fail to load the library. So they travel together, and the documentation gives the canonical form:

1extension_control_path = '/usr/local/share/postgresql:$system'
2dynamic_library_path = '/usr/local/lib/postgresql:$libdir'

Why this landed when it did is the interesting part, and the answer is containers. In an immutable-image world, adding an extension to a running PostgreSQL used to mean rebuilding the base image so the files would land in $system, one rebuild per extension, forever. With these two paths you instead mount a volume of extensions and point the server at it, and an operator like CloudNativePG can offer a new extension without anyone building a new image. The same mechanism covers the smaller cases the feature was also meant for: testing an extension straight out of its build tree, and installing into a non-root prefix on systems like Postgres.app where the system directory is off-limits.

One operational note. extension_control_path is superuser-only. A superuser can change it at runtime, but that change lasts only for the connection and is meant for development; the place it belongs for real use is postgresql.conf. Set it there, set dynamic_library_path next to it, and keep in mind the one fact that trips people first: this parameter finds control files, not libraries.