Search, install, inspect, remove and update software on OpenBSD with three small tools. Every example run for real, including the flavor that needs a choice, the dependency left behind, and the errors.
▶Prefer to watch? This short video runs this page's main commands on a real OpenBSD 8.0 machine, with on-screen captions and a synthetic voiceover (an AI voice trained on the author's own recording). Click to play, or watch it on YouTube.
doas pkg_add -v tree jq # install, with the dependencies
pkg_info -m # the packages you asked for
doas pkg_delete -a # remove the dependencies nobody needs
OpenBSD keeps the base system and the packages apart. The base is updated with syspatch and sysupgrade, and everything else is installed with pkg_add, below /usr/local. This page searches, installs, inspects and removes packages, starts a package service with rcctl, updates with pkg_add -u, and shows the errors.
Every example ran for real on OpenBSD 8.0 (amd64) in a virtual machine, with root commands run through doas. The output you see is the output the machine gave.
Packages on OpenBSD: where they come from
OpenBSD is the base system and nothing else. The compiler, the web server httpd, the firewall pf and the editor vi are all part of the system, and they are updated together with it. Everything else is a package: a signed archive that you install with pkg_add, that lives under /usr/local, and that is tracked in a small database. There is no separate package manager to install. The three tools are in the base system: pkg_add installs, pkg_info asks, and pkg_delete removes.
Every example here ran for real on OpenBSD 8.0 (amd64) in a virtual machine, as an ordinary user, with doas in front of the commands that need root. This is a fresh machine. Where do the packages come from, and what is installed already?
$ uname -sr
OpenBSD 8.0
$ cat /etc/installurl
https://cdn.openbsd.org/pub/OpenBSD
$ pkg_info
vmm-firmware-1.16.3p12 SeaBIOS and EDK II binary images for vmd(8)
$ ls /var/db/pkg
vmm-firmware-1.16.3p12
$ ls /usr/local
bin
include
info
lib
libdata
libexec
man
sbin
share
/etc/installurl holds one line, the address of the mirror. The installer wrote it, and pkg_add, syspatch and sysupgrade all read it. The packages for the release (here 8.0 and amd64) are below that address. Only one package is installed: vmm-firmware, which the installer adds for vmd. Each installed package has a directory in /var/db/pkg, and /usr/local is almost empty, with only the directories that the packages will fill.
Finding the right package
You do not need to know the exact name. pkg_info -Q searches the mirror, and prints every package whose name contains the word. This is the package for the command tree, and what jq looks like:
$ pkg_info -Q tree | grep '^tree'
tree-0.62
tree-sitter-0.27.0
tree-sitter-cli-0.26.5p0
treewm-0.4.5p9
$ pkg_info -Q jq | grep '^jq'
jq-1.8.2
pkg_info -Q needs the network, as it reads the list of the mirror. The first command found tree-0.62, which is the one we want, and some other programs whose names start with the same letters. The name is the program, a dash, and the version. For jq, the only match at the start of the name is jq itself; the other hits of pkg_info -Q jq are things that merely contain the letters. (Once a package is installed, pkg_info -Q marks it with (installed).)
Some programs come in several **flavors**, built with different options. The editor vim is one. If the name is not enough to choose, pkg_add says so, and does nothing. The option -n means "pretend": it shows what would happen and changes nothing, so it is the right way to try:
$ doas pkg_add -n vim
quirks-7.227:updatedb-0p0: ok
quirks-7.227 signed on 2026-11-06T21:34:53Z
Ambiguous: vim could be vim-9.2.1125p0-python3 vim-9.2.1125p0-lua vim-9.2.1125p0-gtk3-perl-python3-ruby vim-9.2.1125p0-perl-python3-ruby vim-9.2.1125p0-ruby vim-9.2.1125p0-gtk3-lua vim-9.2.1125p0-gtk3 vim-9.2.1125p0-gtk3-python3 vim-9.2.1125p0
quirks-7.227: ok
(Here pkg_add only prints the names, because these commands ran without a terminal. On a terminal it does not stop: it shows the same names as a numbered list, "Ambiguous: choose package for vim", with 0: <None> first, and asks "Your choice:".) The order of the names changes from one run to the next, but the list is always the same: plain vim, and the builds with gtk3, python3, perl, ruby or lua. To choose, add the flavor to the name with a dash, or two dashes for the plain one. The two dashes mean "the version without any flavor":
$ doas pkg_add -n vim--
quirks-7.227:updatedb-0p0: ok
quirks-7.227 signed on 2026-11-06T21:34:53Z
quirks-7.227: ok
vim-9.2.1125p0:libiconv-1.19: ok
vim-9.2.1125p0:gettext-runtime-1.0: ok
vim-9.2.1125p0:libsodium-1.0.22: ok
vim-9.2.1125p0: ok
$ pkg_info -Q vim | grep '^vim'
vim-9.2.1125p0
vim-9.2.1125p0-gtk3
vim-9.2.1125p0-gtk3-lua
vim-9.2.1125p0-gtk3-perl-python3-ruby
vim-9.2.1125p0-gtk3-python3
vim-9.2.1125p0-lua
vim-9.2.1125p0-perl-python3-ruby
vim-9.2.1125p0-python3
vim-9.2.1125p0-ruby
vim-classic-8.3.0pl20260916
vim-classic-8.3.0pl20260916-gtk3
vim-classic-8.3.0pl20260916-gtk3-lua
vim-classic-8.3.0pl20260916-gtk3-python3
vim-classic-8.3.0pl20260916-lua
vim-classic-8.3.0pl20260916-python3
vim-command-t-1.11.2p13
vim-spell-af-9.2p0
vim-spell-am-9.2p0
vim-spell-bg-9.2p0
vim-spell-ca-9.2p0
vim-spell-cs-9.2p0
vim-spell-cy-9.2p0
vim-spell-da-9.2p0
vim-spell-de-9.2p0
vim-spell-el-9.2p0
vim-spell-eo-9.2p0
vim-spell-es-9.2p0
vim-spell-fo-9.2p0
vim-spell-fr-9.2p0
vim-spell-ga-9.2p0
vim-spell-gd-9.2p0
vim-spell-gl-9.2p0
vim-spell-he-9.2p0
vim-spell-hr-9.2p0
vim-spell-hu-9.2p0
vim-spell-id-9.2p0
vim-spell-it-9.2p0
vim-spell-ku-9.2p0
vim-spell-la-9.2p0
vim-spell-lt-9.2p0
vim-spell-lv-9.2p0
vim-spell-mg-9.2p0
vim-spell-mi-9.2p0
vim-spell-ms-9.2p0
vim-spell-nb-9.2p0
vim-spell-nl-9.2p0
vim-spell-nn-9.2p0
vim-spell-ny-9.2p0
vim-spell-pl-9.2p0
vim-spell-pt-9.2p0
vim-spell-ro-9.2p0
vim-spell-ru-9.2p0
vim-spell-rw-9.2p0
vim-spell-sk-9.2p0
vim-spell-sl-9.2p0
vim-spell-sv-9.2p0
vim-spell-sw-9.2p0
vim-spell-tet-9.2p0
vim-spell-tl-9.2p0
vim-spell-tn-9.2p0
vim-spell-uk-9.2p0
vim-spell-yi-9.2p0
vim-spell-zu-9.2p0
vim-taglist-4.6p4
vimb-4.0.0
The first command shows what the plain vim would bring along (libiconv, gettext-runtime and libsodium, as dependencies). The second lists all the vim builds. The ones with gtk3 need the X window system, so on a server you want the plain one.
Installing, and what you got
Install two small programs. tree draws a directory as a tree, and jq reads JSON. The option -v prints what pkg_add does, step by step:
$ doas pkg_add -v tree jq
quirks-7.227 (processing)
quirks-7.227:updatedb-0p0 (processing)
quirks-7.227:updatedb-0p0 (extracting)
quirks-7.227:updatedb-0p0 (skipping)
quirks-7.227:updatedb-0p0 (installing)
Adding quirks-7.227:updatedb-0p0
quirks-7.227 (processing)
quirks-7.227 signed on 2026-11-06T21:34:53Z
quirks-7.227 (extracting)
quirks-7.227 (skipping)
quirks-7.227 (installing)
Adding quirks-7.227
tree-0.62 (processing)
tree-0.62 (extracting)
tree-0.62 (skipping)
tree-0.62 (installing)
Adding tree-0.62
jq-1.8.2 (processing)
jq-1.8.2:oniguruma-6.9.10 (processing)
jq-1.8.2:oniguruma-6.9.10 (extracting)
jq-1.8.2:oniguruma-6.9.10 (skipping)
jq-1.8.2:oniguruma-6.9.10 (installing)
Adding jq-1.8.2:oniguruma-6.9.10
jq-1.8.2 (processing)
jq-1.8.2 (extracting)
jq-1.8.2 (skipping)
jq-1.8.2 (installing)
Adding jq-1.8.2
pkg_add checked the signature of the packages (they are signed with signify, and the key for the release is already in /etc/signify), found that jq needs a library, **oniguruma**, and installed that first. It also fetched **quirks**, a small package with the exceptions that pkg_add needs to know about, and installed **updatedb**, a cache that makes pkg_add faster. Both are pkg_add's own helpers, and they stay. Now look at what is there:
$ pkg_info
jq-1.8.2 lightweight and flexible command-line JSON processor
oniguruma-6.9.10 regular expressions library
quirks-7.227 exceptions to pkg_add rules
tree-0.62 print ascii formatted tree of a directory structure
updatedb-0p0 pkg_add speed up cache
vmm-firmware-1.16.3p12 SeaBIOS and EDK II binary images for vmd(8)
$ pkg_info -m
jq-1.8.2 lightweight and flexible command-line JSON processor
quirks-7.227 exceptions to pkg_add rules
tree-0.62 print ascii formatted tree of a directory structure
vmm-firmware-1.16.3p12 SeaBIOS and EDK II binary images for vmd(8)
$ tree -L 1 /etc/ssh
/etc/ssh
|-- ssh_config
|-- ssh_host_ecdsa_key
|-- ssh_host_ecdsa_key.pub
|-- ssh_host_ed25519_key
|-- ssh_host_ed25519_key.pub
|-- ssh_host_mldsa44_ed25519_key
|-- ssh_host_mldsa44_ed25519_key.pub
|-- ssh_host_rsa_key
|-- ssh_host_rsa_key.pub
`-- sshd_config
0 directory, 10 files
$ echo '{"name": "OpenBSD", "version": 8.0}' | jq .name
"OpenBSD"
pkg_info -m lists the packages that **you asked for**. oniguruma is not in that list: it is there only because jq needs it. OpenBSD marks every package as installed manually, or as a dependency, and that mark decides what pkg_delete -a may remove later. These commands tell you more about one package:
$ pkg_info -E /usr/local/bin/jq
/usr/local/bin/jq: jq-1.8.2
jq-1.8.2 lightweight and flexible command-line JSON processor
$ pkg_info -L jq
Information for inst:jq-1.8.2
Files:
/usr/local/bin/jq
/usr/local/include/jq.h
/usr/local/include/jv.h
/usr/local/lib/libjq.a
/usr/local/lib/libjq.la
/usr/local/lib/libjq.so.2.2
/usr/local/lib/pkgconfig/libjq.pc
/usr/local/man/man1/jq.1
/usr/local/share/doc/jq/AUTHORS
/usr/local/share/doc/jq/COPYING
/usr/local/share/doc/jq/NEWS.md
/usr/local/share/doc/jq/README.md
$ pkg_info -R oniguruma
Information for inst:oniguruma-6.9.10
Required by:
jq-1.8.2
$ pkg_info -d jq | head -n 6
Information for inst:jq-1.8.2
Description:
jq is a command-line JSON processor.
It is like sed for JSON data - you can use it to slice and filter and
-E tells which package owns a file, -L lists the files of a package, -R tells who requires it, and -d prints the description. Everything went below /usr/local: the programs in bin, the manual pages in man. The base system is not touched.
Removing, and the leftovers
pkg_delete removes a package. First pretend, with -n, and with -v to see what it would do:
$ doas pkg_delete -nv jq
Deleting jq-1.8.2(pretending)
Removing manpages in /usr/local/man: /usr/local/man/man1/jq.1
$ doas pkg_delete -v jq
Deleting jq-1.8.2
$ pkg_info | grep -E '^(jq|oniguruma)'
oniguruma-6.9.10 regular expressions library
jq is gone, and oniguruma is still there. A dependency stays when you delete the program that needed it. pkg_delete -a removes the packages that nobody needs any more, and nothing that you installed yourself:
$ doas pkg_delete -av
Deleting oniguruma-6.9.10
can't delete updatedb-0p0 without deleting quirks-7.227
Won't delete manually installed vmm-firmware-1.16.3p12
Won't delete manually installed tree-0.62
Won't delete manually installed quirks-7.227
$ pkg_info
quirks-7.227 exceptions to pkg_add rules
tree-0.62 print ascii formatted tree of a directory structure
updatedb-0p0 pkg_add speed up cache
vmm-firmware-1.16.3p12 SeaBIOS and EDK II binary images for vmd(8)
The messages say which packages were kept because they were installed by hand (tree, quirks, vmm-firmware), and that oniguruma was removed. This is the whole story of cleaning up on OpenBSD. When you install and remove programs over a few years, run pkg_delete -a now and then, and the dependencies that nobody uses go away.
Programs that run: rc scripts and readmes
A package that is a daemon needs more than files. nginx is a good example. pkg_add prints a short message at the end, and it is worth reading:
$ doas pkg_add nginx
quirks-7.227 signed on 2026-11-06T21:34:53Z
The following new rcscripts were installed: /etc/rc.d/nginx
See rcctl(8) for details.
New and changed readme(s):
/usr/local/share/doc/pkg-readmes/nginx
Two lines matter. It installed an **rc script**, /etc/rc.d/nginx, and it tells you to look at rcctl(8), which is how OpenBSD starts and stops services. And it put a **readme** in /usr/local/share/doc/pkg-readmes/. Such readmes hold the things that are specific to OpenBSD, for example how nginx runs in a chroot below /var/www and how to rotate its logs:
$ head -n 12 /usr/local/share/doc/pkg-readmes/nginx
+-----------------------------------------------------------------------
| Running nginx on OpenBSD
+-----------------------------------------------------------------------
Log file rotation
=================
To use newsyslog to rotate the log files, you can replace the standard
entries for httpd(8) in /etc/newsyslog.conf with these:
/var/www/logs/access.log 644 4 * $W0 Z /var/run/nginx.pid SIGUSR1
/var/www/logs/error.log 644 7 250 * Z /var/run/nginx.pid SIGUSR1
$ ls /usr/local/share/doc/pkg-readmes
nginx
A package never starts a service by itself. You decide, with rcctl. Enable it (so that it starts at boot), start it, and ask whether it runs:
$ doas rcctl enable nginx
$ doas rcctl start nginx
nginx(ok)
$ rcctl check nginx
nginx(ok)
$ ftp -o - http://127.0.0.1/ 2>&1 | head -n 3
ftp: Error retrieving http://127.0.0.1/: 403 Forbidden
The server runs, and the answer is **403 Forbidden**. That is right: the default web root, /var/www/htdocs, has no index file yet. Make one, as root (the redirection must be done by a shell that runs as root, so the whole command goes to a shell run by doas), and ask again:
$ doas ksh -c 'echo "hello from nginx" > /var/www/htdocs/index.html'
$ ftp -o - http://127.0.0.1/ 2>&1 | head -n 3
hello from nginx
The page is served. To leave the machine as it was, stop and disable nginx, and remove the package:
$ doas rcctl stop nginx
nginx(ok)
$ doas rcctl disable nginx
$ doas pkg_delete -v nginx
Deleting nginx-1.30.6
$ doas rm /var/www/htdocs/index.html
Updating and the mirror
There is one command for updates, and it is the same as for installing, with -u. First pretend, then do it. With no names, it looks at every installed package:
$ doas pkg_add -un
quirks-7.227 signed on 2026-11-06T21:34:53Z
$ doas pkg_add -u
quirks-7.227 signed on 2026-11-06T21:34:53Z
The machine is up to date, so nothing happens, apart from the check of quirks. The first line, "quirks-7.227 signed on ...", is the check of quirks, the helper that holds the exceptions pkg_add needs to know about, for example programs that were renamed. It is always fetched first, and the line also tells you how fresh the mirror is. Where a package came from is written in its packing list:
$ pkg_info -f tree | grep -E '^@(url|signer|option)'
@url https://cdn.openbsd.org/pub/OpenBSD/8.0/packages/amd64/tree-0.62.tgz
@signer openbsd-80-pkg
@option manual-installation
The address has the release in it (8.0), and the package is signed by the key of the release. The mirror of a release has two directories of packages, and you can see them:
$ ftp -o - https://cdn.openbsd.org/pub/OpenBSD/8.0/ 2>&1 | grep -o 'href="packages[a-z-]*/"' | sort -u
href="packages-stable/"
href="packages/"
**packages** is what was built for the release. **packages-stable** gets the security fixes and the updates after the release. pkg_add -u takes care of both, so you do not choose: run it from time to time. The base system is a separate matter: syspatch updates the base (see the guide Updating OpenBSD on this site). When you move to a new release with sysupgrade, run pkg_add -u afterwards, as the old packages are built for the old release.
When it goes wrong
The messages of pkg_add are short. These are the ones you will meet. A package that does not exist, or a name that is spelled wrong:
$ doas pkg_add -n nosuchpkg
quirks-7.227 signed on 2026-11-06T21:34:53Z
Can't find nosuchpkg
pkg_add says "Can't find nosuchpkg", and nothing else happens. Note that in this case the exit status of the command was 0, so a script must not trust the status alone: look at the packages afterwards with pkg_info -e. A version that does not exist gives a longer message:
$ doas pkg_add -n tree-0.1
quirks-7.227 signed on 2026-11-06T21:34:53Z
Can't find tree-0.1
Couldn't install tree-0.1
(exit status 1)
pkg_add must run as root. As an ordinary user it stops at once:
$ pkg_add jq
pkg_add: pkg_add must be run as root
(exit status 1)
A mirror that cannot be reached shows which address failed, and then "Can't find". Here the address is set by hand for one command with PKG_PATH, to a port where nothing listens:
$ doas env PKG_PATH=http://127.0.0.1:1/ pkg_add -n jq
http://127.0.0.1:1/: ftp: connect: Connection refused
http://127.0.0.1:1/: empty
Can't find jq
If you see a message like this when the mirror was fine yesterday, look at /etc/installurl and at the network, and try again. At the end, pkg_check checks the whole package database: that every file is where it should be and that every dependency is installed. It prints nothing when all is well:
$ doas pkg_check; echo "exit status $?"
exit status 0
pkg_check is also what you run after a crash or a full disk in the middle of an installation, when the database may be in a bad state.