I know daemons have to be sent a HUP for config changes to take effect. But I'm wondering why this is, and if it is possible to create a daemon responsive to such changes.
There are multiple reasons. One major reason is that many daemons have multiple configuration files, and any single file change might not be usable on its own — so having a daemon attempt to reload its configuration whenever one of its configuration file changes might cause more problems than it solves.
From a purely implementation-related standpoint, having to watch for changes to configuration files adds more complexity to the daemon. Daemons have a central loop of some sort, checking for work to be done corresponding to the daemon’s core purpose; checking for changes to configuration files doesn’t necessarily fit nicely into that core purpose.
Handling a separate signal solves both of these problems: it indicates that the user thinks the configuration is coherent, and is safe to reload, and it can be implemented asynchronously in a signal handler (typically as a basic flag change), while minimising the impact on the main loop (it reacts to the flag change).
There are daemons which react to configuration changes on their own;
cron for example checks its configuration files for changes every time it goes round its main loop.
Apart from all the other reasons mentioned in the other answers, there is a more deep, philosophical reason, and that is one of the fundamental Unix programming design principles: Do One Thing And Do It Well.
In Unix, programs generally do one thing, and one thing only. More complex operations are achieved by combining multiple programs.
Now, serving web sites (for example) that is one thing. Watching files for changes, that is another thing. So, according to the Unix motto, those should be two different programs, because a web server that e.g. serves web sites and watches files for changes wouldn't be doing One Thing And Do It Well, it would be doing two things. And programs that do two things very often do at least one of those two things not very well. (For example, the person who wrote the web server might be an expert in HTTP, but not an expert in file watching.)
For network-facing daemons specifically, there is also another reason to keep to the Do One Thing mantra: security. Every single line of code is a potential bug. The most secure code is no code. By removing responsibilities from the daemon that do not belong there, you reduce the amount of code, and thus the attack surface.
Apart from the fact that programs that do multiple things often do at least one of those multiple things badly, there is another reason for splitting the responsibilities: code reuse. If file watching were part of the web server, then your SSH server also needs to implement file watching. And your file server. And your chat server. And your telephony server. And your database server. And your media streaming server. And so on, and so forth.
Whereas if file watching is the responsibility of a separate program, then this program only needs to be implemented once, tested once, optimized once, documented once, etc. You also only need to train people once how to use it, and then they can apply it to every single daemon ever written, and in fact even to every single daemon that will be written in the future.
So, if you wanted to do what you ask in your question, you would have two daemons: one that, e.g. serves web sites, and one that watches files for changes and executes an action based on those changes.
And, it probably shouldn't come as a surprise, such daemons already exist.
Adding to other answers: One of the biggest obstacles would be in requiring each configuration value to have an interpretation that's aware of, at minimum, the current state of the application, the previous value, and the other values changed. In general the config file instructs the daemon on how to go from non-running state to its initial state. For the daemon to reload its config on changes means also being able to interpret the config values as modifications to the daemon's current state. For example, let's consider some kind of task daemon. In it's config could be
worker_threads: 2. If the daemon is just starting up, the interpretation is simple: create two threads for executing tasks. If you need to handle a modification to that value, now you need to do multiple things - if the thread count was increased, add more threads. If it was decreased, remove some threads.
Not only is the interpretation of the change dependent on the previous value, it's also dependent on the current state of the daemon. If you update the config to
worker_threads: 1, then what should the daemon do if both threads are in use? Should one be killed to immediately match the state described in the config? Should the tasks be allowed to run to completion and then one thread is killed?
Past that, the interpretation of the config value change is also dependent on which other config values changed. Let's say there's an additional config value to determine what to do when we decrease the worker thread count while all workers are in use:
kill_threads_on_worker_reduction: true/false. Well if that's set we can determine whether to kill the active worker threads or not. But what if that config value was also changed? Does it only apply going forward to new tasks being executed or does it apply retroactively to tasks started before the change?
This is just drilling into one possible config value. You would have to figure this out for every config value, for every possible application state, and for every possible combination of changed values.
While there are certainly cases where the interpretation of config changes are much more straightforward, this helps explain why live-reloading config changes isn't something that generally can be easily done.