My tips:

All tips are subject to costs/benefits analysis, to be broken if necessary. But don't underestimate the costs; it is very easy to do so when metaprogramming. "Can anyone ever understand it again?" is a big deal. "How does the debugger handle it?" and "Can I even get print statements/logging into the critical places?" are also very big deals. "How long does it take a newbie to come use the framework?" and "How long does it take for a newbie to understand the framework to the point they could modify it?" are two separate questions, and you should spend at least a bit of time thinking about the latter one, even if it is not a primary design driver.

Metaprogramming should be additive; once some property is added to some entity, it should generally not be modified or taken away. Some modification forms may be acceptable, like the Decorator pattern, but those should be carefully thought through. Think through naming very carefully before deploying, you will rapidly be stuck with the bad names.

There ought to be a simple path from unadorned program, through the metaprogram, out to the final result. Metaprogramming should not be scattered willy-nilly throughout the code, where if you call this function with these params, suddenly all arrays grow an extra method, but otherwise they don't. Another way of looking at this is that there should be a conceptual "compilation" phase before any "real" code runs, where all metaprogramming has taken effect. (It may not be literal; some things may only take effect on module load, for instance, but if a module load is an atomic event and there's no way to access it "before" the metaprogramming has done its thing, then it's ok even if the module is loaded late at runtime.)

Metaprogramming construct use should be minimal. I work in Perl a lot, which permits a surprising degree of hacking on the interpreter, such as with source filters and the terrifying ability to take over the entire parse process briefly. If you have to use this, OK, but at least try to avoid it first. Try hard. Every construct you add by definition means it is that much harder for the next person to understand how your metaprogram works, and by definition, brings the semantics of your program that much further away from the base semantics of the underlying language out into your own world. This further means that A: the learning curve for your new language just went up and B: you are now out on your own in language design, which is a notoriously tricky area that really benefits from "many eyes". Don't underestimate the importance of that last point! (In perl, so far, I usually manage to write things without any source filter, and with only rare throwing subrefs into a symbol table entry.)

In order to make some things easier, you must make some other thing harder (if only relatively). Keep in mind the difference between "harder" and "impossible"! If you are writing a glorious metaprogramming framework to make it easy to handle querystring parameters, don't forget that people might still legitimately need to access some parameter from multiple handler functions, or legitimately need access to the original querystring. If you are writing a glorious metaprogramming ORM, don't forget that I might need the full power of SQL; does using your ORM mean giving up aggregation or (distressingly common) joins? It doesn't have to be the common case, or the optimized case, but if you make aggregate queries actually impossible then I've got a problem. (How could that happen? If you do something to the name of a column, and don't give me access to the column mapping, then within the program I may not be able to even write my own query. I can lose joins if your framework has excessively strict ideas about controlling your tables and you don't let me put foreign key refs in... or your framework breaks when they are there because it does bad things!) A lot of frameworks I see make too much impossible, and a lot of times it's just gratuitous, easily-fixed.