You can disable direct push in repositories depending on which technologies you're using. Preventing push can be done somewhat using git hooks, but if you're using gitlab/github/bitbucket... there should be a way to protect branches.
If all branches are protected and that the dev can't create new branch, then there is no way to push to the repo. This way, you'll have to handle the dev using "merge requests/push requests" or how it's called.
It's unclear what you're actually managing so the following might not help. In my case, we have a core product that is installed on every client, the core product might be installed with different versions. And additional modules can be installed on the side. Either way, usually client customization are usually done by installing specific modules for clients that extend existing modules (including core modules). In my case, customizations for clients never or rarely bubble up to the core product. If we needed to, we could simply move the module into the core product.
If a custom module is shared across multiple clients, we might use special branches that are merged on the master of each module repository.
In the end, it can get quite complicated if code doesn't bubble up to the master branch of each module's repo. In our case, most of the work is achieved using buildout to manage repos and merges and dependencies.
Handling dependencies using git repositories should be limited to development even if it looks simpler to just fetch the repos. Git repository can get quite big and sometimes fetching might fail if the repository is big and the internet connection is unstable...Git doesn't allow partial fetch so if it fails at 99%, you'll have to restart from 0% anyway.
For deployment, I think it makes more sense to have package managers do it. They exists and they usually do their jobs well. The advantage of using a package manager is that you don't have to download the whole history of changes that might come with the module. Usually package managers allow partial download using http so you can pause an update later if internet fails. With a good package manager, you could also host your own private package servers for each client. Any module that is similar for each client can be pushed on a central server and every customized package could be pushed to a package server per client.
All of this is fine if you have a way to automate the building process of package and deployment of package on their respective servers.
This way if a client does something like "package update", it could check on the client server for a package and then on the central server and take the right version from this. Old package don't have to be deleted and the client could downgrade its setup as he wish.